Uh oh!
There was an error while loading. Please reload this page.
asr: add a 4-bit nf4 option for the Whisper engine - #72
Merged
Conversation
WHISPER_DTYPE=nf4 runs Whisper's weights in bitsandbytes' NF4: 0.53 GB instead of 1.62 GB, same WER as bf16 on the repo's 208-clip corpus (4.36% vs 4.64%, 95% CI [-0.95, +0.31] pp, p=0.39), at 1.7x the decode latency. Activations stay bf16, so it needs Ampere or newer like the default does. The format is NF4 rather than the NVFP4 snapshot the Parakeet engine ships, and the choice is measured. scripts/compare_whisper_quant.py (added here) put four 4-bit options through the same corpus, calibration material, and paired bootstrap the Parakeet precision study used. All four hold WER within noise and all four land at 0.53-0.60 GB of weights, so accuracy and memory do not separate them; latency does. The three nvidia-modelopt formats all cost 4.1x decode latency because they unpack each weight back to bf16 and call an ordinary GEMM, where bitsandbytes fuses the dequantization into its matmul. Parakeet does not pay that — its encoder is launch-bound, so a 2 s clip and a 36 s clip both take ~27 ms — which is why nvfp4 is the right trade there and the wrong one here, with Whisper's decoder generating a token at a time. NF4 also needs no snapshot infrastructure. modelopt quantizes from the loaded bf16 weights and peaks at 1.8 GB, above the model it replaces, which is useless on a card that could not hold bf16 in the first place and is the whole reason server/nvfp4.py exists. bitsandbytes quantizes layer by layer as the checkpoint streams in and peaks at 0.81 GB, so there is no calibration corpus, no pre-quantized download, and no build step: set the variable and the next start is 4-bit. The harness runs each variant in its own subprocess. That is not tidiness — modelopt's calibration leaves ~0.5 GB of live tensors behind that no empty_cache reclaims, and in a shared process that memory lands on whichever variant runs next, which is how the first pass measured NF4 at 1.13 GB when it is 0.53 GB on its own. bitsandbytes is a new dependency, and server/whisper.py deliberately declines alternative runtimes to keep a second CUDA runtime out of the image. This one is a genuine exception: it links against torch's own CUDA rather than shipping its own cuBLAS. Measured on an RTX 5090 (sm_120). Pre-Ampere cards are refused with a message pointing at fp16 rather than failing obscurely; Ampere and Ada were not tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01376qJiterkkQwTWmN1AC4Q
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
WHISPER_DTYPE=nf4runs the Whisper engine's weights in bitsandbytes' NF4 — 0.53 GB instead of 1.62 GB, same WER, 1.7× the decode latency. Activations stay bf16, so it needs Ampere or newer like the default does.Closes the gap the module docstring used to state as fact ("there is no 4-bit option here").
Why NF4 and not the NVFP4 snapshot Parakeet ships
scripts/compare_whisper_quant.py(added here) put four 4-bit options through the same 208-clip corpus, calibration material, and paired bootstrap as the Parakeet precision study. All four hold WER within noise and all four land at 0.53–0.60 GB of weights, so accuracy and memory don't separate them. Latency does:The three nvidia-modelopt formats cost 4.1× because they unpack each weight back to bf16 and call an ordinary GEMM; bitsandbytes fuses dequantization into its matmul. Parakeet doesn't pay that — its encoder is launch-bound, so a 2 s clip and a 36 s clip both take ~27 ms — which is why nvfp4 is the right trade there and the wrong one here, with Whisper's decoder generating a token at a time.
NF4 also needs no snapshot infrastructure. modelopt quantizes from the loaded bf16 weights and peaks at 1.8 GB — above the model it replaces, useless on a card that couldn't hold bf16, which is the whole reason
server/nvfp4.pyexists. bitsandbytes quantizes layer by layer as the checkpoint streams in and peaks at 0.81 GB. No calibration corpus, no pre-quantized download, no build step.No variant produced an empty transcript or any runaway hallucination. 4 bits rewrites the exact text of ~18% of clips without moving the aggregate.
What's here
server/whisper.py—nf4joins bf16/fp16 (aliases4bit,int4)._require_device()factored out ofload()with an nf4-specific message;_nf4_config()builds theBitsAndBytesConfig(double-quant, bf16 compute,proj_outskipped since it shares storage with the token embedding). The quantized branch skips.to()and the dtype check, and instead fails loudly if bitsandbytes hands back an unquantized model — otherwise it would use full bf16 VRAM while claiming 4-bit.scripts/compare_whisper_quant.py— the harness. Reusescompare_precision.py's scoring and paired bootstrap so the two reports are comparable. Refuses to run if the eval and calibration corpora share a clip. Runs each variant in its own subprocess: modelopt's calibration leaks ~0.5 GB of live tensors that noempty_cachereclaims, and in a shared process that lands on whichever variant runs next — which is how the first pass measured NF4 at 1.13 GB.requirements.in/.txt— bitsandbytes, lock regenerated (+10 lines, no resolution churn).scripts/test_protocol.py— the old test assertedint4was rejected, now an nf4 alias. Replaced with alias coverage plus a rejection test that still pinsnvfp4as Parakeet-only..env.example— new "Whisper on a small card — nf4" section with the numbers and the format rationale.Verified
scripts/test_protocol.py— all checks passcreate_asr('whisper')on the GPU: 0.529 GB weights, 0.810 GB build peak, correct transcriptsNot done
🤖 Generated with Claude Code
https://claude.ai/code/session_01376qJiterkkQwTWmN1AC4Q