Uh oh!
There was an error while loading. Please reload this page.
asr: add a 4-bit nvfp4 option for cards short of VRAM - #69
Merged
Conversation
bf16 needs 1.43 GB of VRAM at peak. That is fine on the card this was built for and awkward on a small one, where the model competes with whatever else is resident. PARAKEET_DTYPE=nvfp4 now runs the encoder's weights in NVIDIA's 4-bit float — E2M1 values in blocks of 16, each block carrying its own fp8 scale — for 0.78 GB peak. bf16 stays the default and its load path is unchanged. It costs latency, not accuracy. Over the same 208 clips / 25.8 min as the fp16 work (LibriSpeech test-clean, those utterances under white noise at 10 and 5 dB SNR, babble at 5 dB and near-clipping gain, plus synthetic speech): WER all vs bf16 weights RTF (5090) bf16 2.99% -- 1.31 GB 0.0134 nvfp4 2.86% -0.13 pp [-0.69, +0.30] 0.51 GB 0.0316 The difference is not significant (p=0.69), and the corpus could not resolve one below about 0.3 pp anyway, so this is "no cost we can measure" rather than "identical". What is visible: four bits changes the exact text of 14% of clips while leaving the aggregate alone, and the encoder output sits 5x further from fp32 than bf16 does (relative L2 0.226 against 0.044) with no non-finite activation anywhere. Decode is 2.4x slower. Parakeet's encoder is bound by kernel launches, not arithmetic -- a 2.2 s clip and a 35.6 s clip both take about 27 ms -- so cheaper multiplies buy nothing and the extra unpack per layer is pure overhead. At RTF 0.032 that is still ~30x faster than real time, but nvfp4 is a memory trade and should not be reached for expecting throughput. Measured the same way, int8 saved only 0.52 GB, so four bits is the better deal here, not the riskier one. Unlike bf16 and fp16 this is not a cast: 4-bit scales come from calibrating on real audio, so it ships pre-quantized as a snapshot directory rather than a .nemo, published alongside them in lightware-dev/parakeet-tdt-0.6b-v3 and fetched on first start. Quantizing at load instead would defeat the point -- the GPU would have to hold the bf16 model first, peaking at 2.53 GB, which a card that small cannot do. The packed tensors go straight onto the device and nothing is converted there; the ~12 s of CPU setup around it is a NeMo and modelopt limitation (neither can build the graph without materialising weights), not something the GPU pays for. Nothing in a snapshot is a pickle. torch.load reconstructs arbitrary Python objects, so loading one runs its author's code -- a poor property for a file the daemon downloads at startup. Weights are safetensors, the quantization recipe is JSON, and the one class the recipe names is resolved by attribute lookup against a modelopt-only allowlist. Adds scripts/build_nvfp4_snapshot.py to rebuild it (~30 s, needs a GPU and a calibration corpus); its --verify reloads the result and requires the exact transcripts the in-memory model produced, since a mis-bound scale buffer still yields fluent English. Also fixes two things this touched. The lock's sanity check rejected any nvidia-* package, which was right when they all came from the cu130 index but not for nvidia-modelopt; it now checks the exclusion list itself. And the "missing fp16 checkpoint" test was vacuous -- with a network the loader downloaded the checkpoint it was told was missing and raised nothing, so the check only looked green because CI has no torch and skips it.
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.
PARAKEET_DTYPE=nvfp4runs the encoder's weights in NVIDIA's 4-bit float (E2M1 in blocks of 16, each block carrying its own fp8 scale), for 0.78 GB peak VRAM against bf16's 1.43 GB. bf16 stays the default and its load path is unchanged.It costs latency, not accuracy
Same 208-clip corpus as the fp16 work:
Not significant (p=0.69) — and at 208 clips the corpus could not resolve a difference below ~0.3 pp, so this is "no cost we can measure", not "identical". Four bits does change the exact text of 14% of clips while leaving the aggregate alone, and pushes the encoder output 5x further from fp32 than bf16 (relative L2 0.226 vs 0.044), with no non-finite activation anywhere.
Decode is 2.4x slower. The encoder is bound by kernel launches, not arithmetic — a 2.2 s clip and a 35.6 s clip both take ~27 ms — so cheaper multiplies buy nothing and the extra unpack per layer is overhead. Still ~30x faster than real time. This is a memory trade; don't reach for it expecting throughput. (int8, measured the same way, saved only 0.52 GB — four bits is the better deal here, not the riskier one.)
Why a snapshot rather than a .nemo
4-bit scales come from calibrating on real audio, not a cast, so it ships pre-quantized and is fetched from
lightware-dev/parakeet-tdt-0.6b-v3on first start like the other two. Quantizing at load would defeat the point: the GPU would hold the bf16 model first, peaking at 2.53 GB, which a card that small cannot do. Packed tensors go straight onto the device and nothing is converted there.No pickles.
torch.loadreconstructs arbitrary Python objects, so loading one runs its author's code — poor for a file the daemon downloads at startup. Weights are safetensors, the recipe is JSON, and the single class it names resolves by attribute lookup against a modelopt-only allowlist.Verification
scripts/build_nvfp4_snapshot.py --verifyreloads the finished snapshot and requires the exact transcripts the in-memory model produced (20/20) — a mis-bound scale buffer still yields fluent English, so plausibility proves nothing..nemo) and transcribes correctly.release_cache().scripts/test_protocol.pypasses, with 6 new precision checks.Two adjacent fixes
nvidia-*package — correct when they all came from the cu130 index, wrong fornvidia-modelopt. It now checks the exclusion list itself.main, confirmed by running the suite there.🤖 Generated with Claude Code
https://claude.ai/code/session_01EnL4vH3w19nXYjAUqFT2jF