QTensor::quantize on a narrowed view quantizes the parent (silently in release, debug_assert in debug)
In a release build, QTensor::quantize on a narrowed-then-contiguous()
tensor quantizes the parent's data instead of the narrowed rows, with no error.
In a debug build the same code panics on a debug_assert_eq! instead.
I am reasonably confident about the mechanism but genuinely unsure whether you consider this a bug or working-as-intended, so I have written down what I measured and left the conclusion open — see "Is this a bug?" at the end.
Environment: CPU backend, main at
6f74e7c390c717f8fd34f23ce02aceb058173370, Apple M1 Max / macOS 26.6. Nothing
here is Metal- or arch-specific; both pieces of code are backend-independent, but
CPU is the only backend I ran it on. Q8_0 and Q4_0 both checked.
Reproduction branch:
oetiker/candle@repro/narrowed-view-quantize-offset
(commit 34d008cc, one test in candle-core/tests/quantized_tests.rs, no fix).
What I measured
Four rows of 32 f32; row r is filled with the value r + 1. Narrow to row 1,
so the correct answer is thirty-two 2.0s. Reading that same view through the
ordinary tensor API returns 2.0, so the view itself is fine.
Release profile (cargo test --release) — no error, wrong data:
assertion `left == right` failed: quantize read the parent from element 0 instead of the narrowed view at offset 32
left: [0.99993896, 0.99993896, ... 32 values ...]
right: [2.0, 2.0, ... 32 values ...]0.99993896 is Q8_0's round-trip of 1.0 — i.e. the parent's row 0, not the
row that was narrowed to.
Debug profile (cargo test) — panics instead, inside the quantizer, at
candle-core/src/quantized/k_quants.rs:666:
assertion `left == right` failed: size mismatch 128 1 32
left: 1
right: 4128 is the parent's element count against a 32-element shape.
The reason the two profiles differ is that the guard at k_quants.rs:666 is a
debug_assert_eq!, so it is compiled out in release and the quantizer proceeds.
Mechanism, as far as I can tell
Two pieces of behaviour compose:
Tensor::contiguous(candle-core/src/tensor.rs:2475-2486) returnsself.clone()whenis_contiguous()is already true. A row-narrowed tensor is contiguous, socontiguous()does not copy it — the result keeps the parent's storage and a non-zerostart_offset(). (force_contiguous()is the one that always copies.) On its own this seems entirely reasonable for consumers that honour the layout.QTensor::quantize(candle-core/src/quantized/mod.rs:543-556) passessrc.storage()— the whole parent storage — to the quantizer, and usesshape.elem_count()only to size the destination. The layout offset is not applied.
Composed, quantize(&t.narrow(..)?.contiguous()?, ..) reads the parent's first
shape.elem_count() elements from element 0.
force_contiguous() produces the correct result, and the test pins that too —
same tensor, same values, right answer once the offset has been resolved by a
copy. That is what makes me fairly sure of the diagnosis rather than just the
symptom.
Why I noticed
Per-expert quantization of an MoE weight stack is naturally written as "narrow to
expert i, make it contiguous, quantize". That code uses only public API and
calls the function whose job is to remove layout concerns. It cost me a full
debugging cycle, most of it spent suspecting an entirely innocent quantization
kernel, because the debug assert fires inside the quantizer and its message
mentions neither narrow nor contiguous nor the offset.
Is this a bug?
I can see a reasonable position where it is not, and I would rather ask than assume:
If the contract is "
quantizerequires asrcwhose storage matches its shape, and thedebug_assertis how that contract is enforced", then this is working as intended and the gap is documentation plus the fact that the release build does not enforce it. In that case the cheap improvement is promoting thedebug_assert_eq!atk_quants.rs:666to a realassert!, so release fails loudly rather than returning wrong data — and perhaps a note onquantizethatcontiguous()is not sufficient andforce_contiguous()is what is wanted.If
quantizeis meant to accept any tensor, then applying the layout offset inQTensor::quantizewould be the narrow fix.
There are wider levers — making quantize call force_contiguous(), or
revisiting what contiguous() guarantees — but the latter has a large blast
radius and I doubt it is the right one.
I am happy to send a PR for whichever of these you prefer, including the "promote the assert and document it" version if that is the answer.
Reproducing
git clone https://github.com/oetiker/candle && cd candle
git checkout repro/narrowed-view-quantize-offset
cargo test --release -p candle-core --test quantized_tests quantize_a_narrowed_view # wrong data
cargo test -p candle-core --test quantized_tests quantize_a_narrowed_view # debug_assert panicCPU only, no accelerator needed.
Source: huggingface/candle