fix(FIX-READ-F32-SCALAR-GUARD): a per-tensor scale is ONE F32 element, and six copies of the reader had stopped saying so (#1181) - #1182
Merged
Conversation
…s neither the element count nor the dtype (#1181) `ReadF32Scalar` bounds its input with `t.nbytes >= sizeof(float)`, a LOWER bound, then copies four bytes into a `float`. An array passes and is read as element 0. Any dtype passes and is reinterpreted. Both return a finite, plausible float, so the model produces fluent wrong tokens rather than failing, which is the one failure mode a token gate cannot see. The audit corrects the issue's own framing twice. The 27 grep hits are 5 definitions, 20 call sites and 2 comment references, and both counts are short: `ReadCtF32Scalar` in `dense_weight_loaders.h` is a sixth copy under another name, reached from a sixth model file through `LoadCtNvfp4W4A16`. And the defect is not only latent. `unsloth/Qwen3.6-27B-NVFP4` @`ccdaab7e` ships FP8 W8A8 throughout with BF16 per-output-channel scales, both facts already recorded in this tree, and `LoadAttnDense` branches on the weight dtype alone, so those projections reach both defects at once under the tensor name the loader actually asked for. No call site legitimately passes a multi-element or non-F32 tensor, and every existing fixture emits rank-0 or `{1}` `F32`, so nothing in the tree needs the leniency that is being removed. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…CLEAN through LoadQwen3_5Moe (#1181) Six refusal subcases plus a positive control, all driven through the production `vllm::LoadQwen3_5Moe` over a synthetic checkpoint rather than a hand-built `StTensor`, so they measure a capability rather than a class. RED at this commit: 16 of 29 assertions fail, 6 of 6 refusal subcases fail, the positive control passes. The shape of the red is the finding. A per-output-channel `weight_scale` `[8] F32`, a block-wise grid `[2, 4] F32`, a per-output-channel `[8] BF16` and a multi-element `weight_scale_2 [2] F32` all LOAD, with an EMPTY exception message. Eleven of the failed assertions report `message := ` with nothing after it. The checkpoint is accepted, every tensor is found, every dtype the loader asks about is right, and the only wrong thing is a number that will be multiplied into every token. The two one-element BF16 cases do stop, on `scalar tensor too small for f32` at `qwen3_5_weights.cpp:313`, which names no tensor and describes a truncation rather than a dtype. Eight BF16 values are sixteen bytes, so that floor cannot see the shape that is actually published. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…, and six copies of the reader had stopped saying so (#1181) `ReadF32Scalar` bounded its input with `t.nbytes >= sizeof(float)` and then copied four bytes into a `float`. A floor admits every wrong answer that is large enough, so an array was read as element 0 and any dtype was reinterpreted rather than converted, and both returned a finite plausible number. A plausible wrong scale yields fluent wrong tokens rather than a failure, which is the one defect class a token gate cannot see. One implementation now, `dense_loaders::ReadF32Scalar(get, name)`. It refuses `numel != 1` naming the shape and the count, refuses a non-F32 dtype naming the dtype, and requires exactly four readable bytes. The name is in the signature so a refusal can never be anonymous, which is what the old `scalar tensor too small for f32` was. Five local copies are deleted onto it: `qwen3_5_weights.cpp`, `qwen3_5_dense_weights.cpp`, `laguna_weights.cpp`, `laguna_shared_fp4.cpp`, and `ReadCtF32Scalar` in `dense_weight_loaders.h` itself, which the issue did not list and which a sixth model file reaches through `LoadCtNvfp4W4A16`. `nemotron_h_weights.cpp:557` stays as it is: its checks are already a superset, and it carries `Loader` bookkeeping the resolver-based helper has no access to. That is the one tracked exception. A narrow dtype is refused rather than converted. Upstream converts by value, so conversion would be defensible, but no caller here can be shown to need it: a one-element BF16 scale has never been read correctly on this path, and the BF16 layout that IS published is per-output-channel, which the count check refuses first. Converting would be untested code on a path no checkpoint reaches. Mirrors `PerTensorScaleParameter` at pin `555967922`, which asserts `loaded_weight.shape[0] == 1` before it copies (`vllm/model_executor/parameter.py:304-309`), allocates the slot `torch.float32` (`utils/fp8_utils.py:1276`), and picks the parameter type from the declared TENSOR / CHANNEL / BLOCK strategy before a byte is read (`compressed_tensors_w8a8_fp8.py:63,128`). FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
…ng the check that is NOT gated (#1181) RED before at `8cff76113` was 16 of 29 assertions and 6 of 6 refusal subcases, and eleven of those failures logged an EMPTY exception message, which is the finding: four wrong-scale shapes did not read the wrong element, they loaded and returned a complete model. GREEN after at `60aefbca6` is 29 of 29 focused, 67,845 of 67,845 for the file, 511 of 511 for ctest, and 81 of 81 preflight gates with no skip. Five mutations, each with its `compile_rc` and its applied diff stat, restored byte-identical. Two reachability mutations delete the production call sites in `LoadFp8Transposed` and `LoadNvfp4Raw` and both go red, so the guard is measured as reached from `LoadQwen3_5Moe` rather than as a class that works. The fifth mutation stayed GREEN and is recorded as such. Deleting the exact-byte-count check changes nothing the fixture can see, because the safetensors reader already rejects a header whose shape times dtype width disagrees with its span, so the count and dtype checks imply four bytes for any file that parses. The line is kept for its null-pointer half and named here as ungated rather than presented as tested. FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: AGENT:claude-opus-5 [claude-code]
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 free
to 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.
ReadF32Scalarbounded its input witht.nbytes >= sizeof(float)and thencopied four bytes into a
float. A floor admits every wrong answer that islarge enough, so an array was read as element 0 and any dtype was reinterpreted
rather than converted. Both return a finite plausible number, and a plausible
wrong scale yields fluent wrong tokens rather than a failure, which is the one
defect class a token gate cannot see.
The audit is the load-bearing half, and it corrects the issue twice
The 27 grep hits across the five named files are 5 definitions, 20 call sites
and 2 comment references, and both counts are short.
There is a sixth definition.
ReadCtF32Scalar(
include/vllm/model_executor/models/dense_weight_loaders.h:376) carried theidentical defect under a different name, inside the shared header that exists to
stop exactly this, and it is reached from a sixth model file the issue does
not list:
src/vllm/model_executor/models/qwen3_weights.cpp:100,126-128throughLoadCtNvfp4W4A16. A grep for one identifier measured five sixths of a classdefect.
Of the six, three checked nothing,
LnReadF32ScalarandShReadF32Scalarchecked the dtype but bounded the size with
nbytes >= 4, and onlynemotron_h_weights.cpp:557-573was correct, which is why it is the referencethis row generalizes rather than a seventh thing to fix.
No call site legitimately passes a multi-element or non-F32 tensor. All 21
read a scale that is per-tensor by contract, and every existing fixture emits
rank-0 or
{1}F32, so nothing in the tree needed the leniency being removed.The full site-by-site table is in the spec.
It is not merely latent, and the counterexample was already in the tree
The issue calls the defect latent because #1166's name miss stops the measured
Qwen/Qwen3.8-27B-FP8load first. True for that checkpoint, false for the class.dense_weight_loaders.h:73-74records thatunsloth/Qwen3.6-27B-NVFP4@
ccdaab7e"went FP8 across the whole tower with BF16 per-output-channelscales", and
docs/BENCHMARKS.md:52records the same revision as FP8 W8A8throughout.
LoadAttnDensebranches on the weight dtype alone(
qwen3_5_dense_weights.cpp:478-480), so those projections enter the per-tensorarm and hit both defects at once under the tensor name the loader actually
asked for. Nothing is misspelled and nothing stops it.
No recorded gate covers it: every recorded 27B NVFP4 measurement ran
@
890bdef7. So this turns a silent wrong scale into a named refusal on a pathno gate was reading. If a GPU run of @
ccdaab7estarts refusing after thislands, that refusal is the discovery and not the regression, and it belongs to
the owed per-output-channel FP8 arm.
The change
One implementation,
dense_loaders::ReadF32Scalar(get, name). It refusesnumel != 1naming the shape and the count, refuses a non-F32 dtype naming thedtype, and requires exactly four readable bytes. The name is in the signature so
a refusal can never be anonymous, which is what
scalar tensor too small for f32was. Five copies are deleted onto it.nemotron_h's twin stays as the onetracked exception, because its checks are already a superset and it carries
Loaderbookkeeping the resolver-based helper cannot reach.A narrow dtype is refused rather than converted. Upstream converts by value, so
conversion would be defensible, but no caller can be shown to need it: a
one-element BF16 scale has never been read correctly here, and the BF16 layout
that IS published is per-output-channel, which the count check refuses first.
Mirrors
PerTensorScaleParameterat pin555967922, which assertsloaded_weight.shape[0] == 1before it copies(
vllm/model_executor/parameter.py:304-309), allocates the slottorch.float32(utils/fp8_utils.py:1276), and picks the parameter type fromthe declared TENSOR / CHANNEL / BLOCK strategy before a byte is read
(
compressed_tensors_w8a8_fp8.py:63,128).Evidence
RED at
8cff76113: 16 of 29 assertions, 6 of 6 refusal subcases. Eleven ofthose failures logged
message :=with nothing after it. A per-output-channel[8] F32, a block-wise grid[2, 4] F32, a per-output-channel[8] BF16and amulti-element
weight_scale_2 [2] F32did not read the wrong element, theyloaded. The two one-element BF16 cases stopped on
scalar tensor too small for f32, which names no tensor and describes a truncation, and only firesbecause one bf16 value is two bytes.
GREEN at
60aefbca6: focused 29/29, whole file 19 cases and 67,845/67,845,ctest511/511 passed, 0 failed,scripts/agent-preflight.sh --fail-on-skip79 ok, 0 FAIL, 0 SKIP, both committed-range blocks executed.Five mutations, each with
compile_rcand its applied diff stat, restoredand verified byte-identical by
sha256sum:compile_rcLoadFp8Transposed's call siteweight_scalesubcasesLoadNvfp4Raw's call siteweight_scale_2subcasesG3 is recorded as a negative result rather than quietly kept. The
exact-byte-count check is not independently gated: the safetensors reader
already rejects a header whose shape times dtype width disagrees with its span,
so the count and dtype checks imply four bytes for any file that parses. The
line is kept for its
t.data != nullptrhalf, which guards adefault-constructed
StTensor, and it is named as ungated rather than presentedas tested.
Reachability per
.agents/reachability.mdis R1 and R2. Every case runs theproduction
vllm::LoadQwen3_5Moeover a synthetic checkpoint, never ahand-built
StTensor, so deleting a production call site turns them red.Closes #1181.
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: AGENT:claude-opus-5 [claude-code]