feat(qwen4_exp): load block-fp8 dense projections natively - #428
Conversation
|
Happy to see this land properly — One behaviour question, not an objection.
if scheme is None:
if is_fp8:
raise ValueError(f"{name} is {tensor.dtype} but the checkpoint's quant config declares {module} unquantized")So a checkpoint that stores block-fp8 dense weights without declaring them per module now fails to load. #320 dequantized that case to bf16 at load and served it, which is how I first got the lovedheart build running — my early notes on it say the block-fp8 attention/GDN tensors were not flagged in Raising it only because the failure mode changes from "loads, dequantized" to "raises", and community requants are the population most likely to store quantized weights without describing them. If that's a deliberate call — an explicit error beats a silent divergence between the buffers the model built and what the reader emits — then it's the right one and worth a line in the docstring so the next person doesn't re-add a fallback. If it isn't, a On the run you asked for: yes. My box is the regime this feature is actually for — a single 24 GB RTX PRO 4000 Blackwell on a PCIe gen 3 board, offload backend, where the model is nowhere near resident and expert-cache residency is the binding constraint. That's the opposite end from your H100 80GB and @gdevenyi's 2 x 48 GB Ada, where the freed dense bytes have much less to buy. On the earlier form of this reader I measured 27.4 -> 34.2 tok/s there, and the gain tracked Also flagging, since #392's reader half is superseded by this: the |
b16877b to
540b2ce
Compare
|
Deliberate. Since #418 a module the config does not list is built bf16, so an fp8 tensor there means the checkpoint's declaration and its tensors disagree. We treat that as a broken checkpoint, not a layout to support; the error names the module to fix. Workaround: add the module to quantization_config.quantized_layers ({"quant_algo": "FP8_PB_WO", "group_size": 128}) in config.json, and in hf_quant_config.json if present. Thanks for the run and for splitting out #429. |
|
Ran it. The reader works — I can't give you decode numbers, though, because this tree runs 40-60x slower than the build I'm coming from on this configuration, and I think that matters more than the numbers I owed you. What I measuredSame box, same checkpoint, same flags, back-to-back:
End-to-end, counting every streamed delta (this model emits most tokens as What it looks like while it is slow
What I can't tell youI cannot attribute this to #428, and I do not think it is #428. My baseline is I also could not get a stack. Why this configuration is probably not coveredYou tested on an H100 80 GB and @gdevenyi is on 2 x RTX 6000 Ada 48 GB. On both, this model is close to resident and the offload expert path barely engages. Here it carries essentially the whole model — 123 GiB of weights against 24 GB of VRAM on a gen-3 board, which is the regime the offload backend exists for, and the one where the dense-side savings in this PR are worth the most. If the slowdown is in that path, it would be invisible on either of your boxes and fatal on mine. Happy to bisect |
|
Narrowed it, and it is not this PR — moved to #436 so it does not sit on your thread. It is
So this PR's reader is fine on my hardware — the The tell is that under |
|
I cannot give you the
which is exactly the failure I spent a GPU window on in the Merge state against our tree. Merging
The one thing to decide before this lands. Line 205: if get_tp_info().size > 1:
raise NotImplementedError("qwen4_exp weight loading supports TP=1 only")That is correct for main today and it is a hard stop for #385. Whichever of the two lands second owns removing it. I am happy for that to be my problem — I mention it only so the guard is a known hand-off and not a surprise. Also note #429 ( Not adopting it into our deployment yet, to be explicit about why: our candidate branch has four post-merge fixes on it and has not completed a GPU startup since. A 429-line reader rewrite goes in after that is validated, not before. Our checkpoint has bf16 dense projections, so this PR changes nothing for us functionally — it is the design I want under #389, not a capability we are missing. 🤖 Generated with Claude Code |
…r's shape FlashML-org#438 folds qwen3_5_moe's four dense readers into one _DenseReader that asks the checkpoint's QuantConfig what each Linear stores, and deletes _iter_weights_attn_fp8 -- which is where --spec-mtp's weight reading lived. The head is read again on the new shape: _rename keeps mtp.* when the engine asks for it, the head's two pre_fc norms and its own final norm join the (1+w) list, and its per-expert bf16 experts are gathered on the host and yielded last as the two stacked tensors the engine quantizes into a bank layer. Two things the old path needed code for are now free. mtp* is off every quantizer's list, so scheme_for_name returns None for the head and it reads as stored -- the table that said "the head's q|k|v are bf16 where the decoder's are fp8" is gone. And the up-front refusal ("MIXED_PRECISION only") went with attn_quant, which FlashML-org#438 stops parsing for this family: the reader is layout-general now, so what is left to refuse is a head that is not in the checkpoint (the stacker's assert) or one the exporter quantized (a loud NotImplementedError -- the engine quantizes these itself, so a pre-quantized head would be stacked into the wrong format). FlashML-org#428 reads qwen4_exp's dense projections through the same QuantConfig. Its _DenseFuser replaces the fork's fuse_buf; the progress bar keeps asking the pipeline engine's rank rather than TP's. FlashML-org#367 reserves paged KV at allocation granularity -- the same page-span accounting this fork already does for the SWA pool, on the other currency. Admission is stricter than the token math by up to one page per request, which is the point of it. FlashML-org#411 honors the server's max_output_tokens in both APIs' defaults; it lands beside the image parts in chat_request_to_genspec without touching them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…layer it will become FlashML-org#428's dense fuser asks the QuantConfig what the model built a projection from and refuses the tensor whose dtype disagrees. Under --dense-quant those are two different answers on purpose: LoadTimeFp8Config reports every projection it is going to convert as per-row fp8, because that is what the layer becomes, while the shard still holds bf16 and the engine converts it in _quantize_at_load -- after the reader has handed it over. So the first GDN projection of the official Flash-Next checkpoint stopped the boot on both ranks: model.layers.0.linear_attn.in_proj_qkv.weight is torch.bfloat16 but the checkpoint's quant config declares model.layers.0.linear_attn.in_proj_qkvz QuantScheme(fp8_tensor, ...) The check now asks what the checkpoint stores that part under (scheme_for_name on the part's own checkpoint name, the same question qwen3_5_moe's _DenseReader.stored() asks), while the GDN in_proj layout keeps coming from the built scheme -- that one has to match gdn.py, which splits qkv|z from b|a exactly when scheme_for says the qkvz half is quantized, at load or in the file. This is the collision the FlashML-org#427 sync predicted and the assert in test_engine_config_dense_quant_override was placed for: get_quant_config() hands the readers the wrapped config, and FlashML-org#428 is its first consumer. Upstream has no --dense-quant, so upstream cannot see it; the new case reads a RadixArk-shaped bf16 checkpoint through the wrapper and pins both halves -- as-stored dtypes and the split layout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…reservation and FlashML-org#428 FlashML-org#367 changes PrefillAdder's KV reservation from raw tokens to whole pages, in the file this box's admission behaviour hangs on (prefill.py reserves input_len + max_tokens up front, which is what caps concurrency here rather than --max-running-requests). On this box it is numerically the identity. page_size is 1 both by default (engine/config.py:66) and in the running serve's own ServerArgs line, and at page_size 1 the new div_ceil(total, 1) - div_ceil(cached, 1) is exactly the old extend_len + output_len -- remain_len is defined as input_len - cached_len two hunks down. It bites where page_size is forced above 1: engine.py:308 (kpool indexer layout, 64) and engine.py:1282 (DSV4, 128). FlashML-org#428 is qwen4_exp, which is not the model this box serves; it rides along with main. Suite: 8 failed, 1,922 passed, 92 skipped. The same eight by name as freetoken-systest results/20260910-suite-instr-merge.txt, not merely the same count -- test_e4m3_compat appears in that file's warnings summary, not its failures. Assisted-by: Claude Opus 5
Loads modelopt MIXED_PRECISION Qwen3.8-Flash-Next checkpoints whose attention / GDN projections are 128x128 block-fp8 (
FP8_PB_WO), e.g.lovedheart/Qwen3.8-Flash-Next-NVFP4-FP8. The model already builds the fp8 buffers (#418); the reader still fused GDNin_proj_{qkv,z,b,a}into one bf16 tensor and died intorch.catwhenin_proj_qkvwas fp8._DenseFuserreplaces_FUSIONS/_try_fuse: parts come frompacked_modules_mapping,.weightand.weight_scale_invfuse per kind, GDN splits intoin_proj_qkvz(fp8) +in_proj_ba(bf16) when the QuantConfig says so.Tests: synthetic bf16, NVFP4 and block-fp8 checkpoints in
tests/models/qwen4_exp/test_weight.py(emitted keys == model state dict, fp8 slice-back, four rejection cases); scheme resolution intest_config.py.Tested on H100 80GB / Xeon 8480+, driver 580.95.05, torch 2.11.0+cu130, TP=1, offload +
--moe-cache-auto:lovedheart/Qwen3.8-Flash-Next-NVFP4-FP8passestests/e2e/test_aime.py, all 926 emitted tensors match the model's buffers, 18 fused fp8 tensors are bit-exact against the raw shards; RadixArk and nvidia NVFP4 unchanged.FREETOKEN_TEST_MODEL=<dir> FREETOKEN_TEST_MOE_CACHE_AUTO=1 pytest -x -s tests/e2e/test_aime.py.Depends on #427. Supersedes #392 / #320 by @gberasmus87, which reached the same reader design before #418. @gberasmus87 @gdevenyi: a run on your cards would be a useful check.