Skip to content

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
localai-bot merged 4 commits into
mainfrom
row/FIX-READ-F32-SCALAR-GUARD
Aug 18, 2026
Merged

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
localai-bot merged 4 commits into
mainfrom
row/FIX-READ-F32-SCALAR-GUARD

Conversation

@localai-bot

Copy link
Copy Markdown
Collaborator

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. 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 the
identical 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-128 through
LoadCtNvfp4W4A16. A grep for one identifier measured five sixths of a class
defect.

Of the six, three checked nothing, LnReadF32Scalar and ShReadF32Scalar
checked the dtype but bounded the size with nbytes >= 4, and only
nemotron_h_weights.cpp:557-573 was correct, which is why it is the reference
this 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-FP8 load first. True for that checkpoint, false for the class.

dense_weight_loaders.h:73-74 records that unsloth/Qwen3.6-27B-NVFP4
@ccdaab7e "went FP8 across the whole tower with BF16 per-output-channel
scales", and docs/BENCHMARKS.md:52 records the same revision as FP8 W8A8
throughout. LoadAttnDense branches on the weight dtype alone
(qwen3_5_dense_weights.cpp:478-480), so those projections enter the per-tensor
arm 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 path
no gate was reading. If a GPU run of @ccdaab7e starts refusing after this
lands, 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 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 scalar tensor too small for f32 was. Five copies are deleted onto it. nemotron_h's twin stays as the one
tracked exception, because its checks are already a superset and it carries
Loader bookkeeping 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 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).

Evidence

RED at 8cff76113: 16 of 29 assertions, 6 of 6 refusal subcases. Eleven of
those 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] BF16 and a
multi-element weight_scale_2 [2] F32 did not read the wrong element, they
loaded. The two one-element BF16 cases stopped on scalar tensor too small for f32, which names no tensor and describes a truncation, and only fires
because one bf16 value is two bytes.

GREEN at 60aefbca6: focused 29/29, whole file 19 cases and 67,845/67,845,
ctest 511/511 passed, 0 failed, scripts/agent-preflight.sh --fail-on-skip 79 ok, 0 FAIL, 0 SKIP, both committed-range blocks executed.

Five mutations, each with compile_rc and its applied diff stat, restored
and verified byte-identical by sha256sum:

Mutation diff stat compile_rc Result
R1 reachability: delete LoadFp8Transposed's call site +1 -1 0 RED, 11/29, the 4 weight_scale subcases
R2 reachability: delete LoadNvfp4Raw's call site +1 -1 0 RED, 5/29, the 2 weight_scale_2 subcases
G1: delete the element-count check +1 -4 0 RED, 7/29
G2: delete the dtype check -3 0 RED, 2/29
G3: delete the exact-byte-count check -3 0 GREEN 29/29

G3 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 != nullptr half, which guards a
default-constructed StTensor, and it is named as ungated rather than presented
as tested.

Reachability per .agents/reachability.md is R1 and R2. Every case runs the
production vllm::LoadQwen3_5Moe over a synthetic checkpoint, never a
hand-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]

mudler added 4 commits August 18, 2026 03:55
…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]
@localai-bot
localai-bot merged commit f22c6cc into main Aug 18, 2026
13 of 21 checks passed
@localai-bot
localai-bot deleted the row/FIX-READ-F32-SCALAR-GUARD branch August 18, 2026 04:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ReadF32Scalar accepts an ARRAY and any dtype, silently returning a wrong scale (27 call sites)

2 participants