Repository navigation
npu: a launch's result is dequantised with one multiply per element, not one ldexp(): prefill 24.4-25.1 -> 29.8-30.6 tok/s on the served 27B pack (Refs #1990) - #2008
Conversation
…not one ldexp() (Refs #1990) 2^e_ref depends only on the column, and it was a libm call per element after readback, where no launch phase column counted it: 137 ms of a 630 ms prefill block on the served 27B pack. It is now computed once per column. Same bits (exact power-of-two multiply; out-of-range exponents keep the per-element form): NPU_DEQ_LDEXP=2 counts differing elements, and npu_pb16i_layout_test holds the two forms equal. The [bl] line names the term as deq. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
ready for review at 93d5843 |
bong-water-water-bong
left a comment
There was a problem hiding this comment.
VERDICT at 93d5843: READY
Checked at 93d5843. A three-way merge onto main is clean.
The "same bits" claim holds, by reading and by test. scale_i64(s, e) is (float)std::ldexp((double)s, e), and the table form is (float)((double)s * ldexp(1.0, e)). Both take the same (double)s first (so a two-plane sum past 2^53 rounds identically), scale once by an exact power of two, and round to float once before * asc. So they agree whenever the product is a normal double. With |s| < 2^63, that holds for e in [-1022, 960], which is what the guard tests. The two-plane recombination is sum_of()'s (hi << 21) + lo, verbatim.
tests/npu_pb16i_layout_test.cpp(NPU-private target, compiled directly with g++-16): "ok (8 geometries)". The new arm covers both planes, the int32 extremes, values past 2^53, and exponents at and beyond both table edges.- Mutants:
kExpHi = 1100fails "exponents above the table range … (3 of 128 differ)", because0 * infgives NaN whereldexp(0, e)gives 0;kExpLo = -1100still passes. That's correct, not a gap: below 2^-1022 the double product andldexpboth land far under float's range and round to the same float. The low bound is conservative.
npu/target/xdna/xdna_target.cppandnpu/forward/src/q4nx_forward.cppcompile at this head (single-file, with an existing NPU build's commands pointed at this tree).
The measurement reads the same way as #2005's, and is written up just as honestly: only A1→B1 is claimed. The box slowed through B2/A2, and with the old form run first, drift would count against the change. The device check ([deqcheck] … differing=0 over 568 M elements) plus the identical greedy sha256 across arms is the right evidence for a "same bits" change.
|
VERDICT: READY at Why this was hard to see, and why the fix is safe
The ask, which is about the instrumentThe phase columns should gain an entry for post-readback host work — otherwise the next per-element call hides in the same gap. A perf fix that leaves the measurement blind to its own class will be followed by the same bug somewhere else. |
Refs #1990 (second host term; this is the one #2005's "What remains" called unnamed).
What it was
Every launch's result is turned into floats as
float32(S * 2^e_ref) * ascale.2^e_refdepends only on the column, but it was computed with a libmldexp()call per element. That code runs after readback, so none of the launch phase columns (pack,ql,wait,rb) counted it, and the batched[lh]table printed it under UNATTRIBUTED.A 16-row prefill block on the served 27B pack is 416 launches of up to 16 x 17,408 elements. Measured, this was the block's largest host term.
What changes
q4nx_pb16i::dequant_rows()computes the power once per column and multiplies.I8Ctx::dequant_pb16i()calls it, so the layer launches and the head both use it. A second zero-fill of the output buffer before the dequant is dropped (the buffer is already sized, and every element is written).The result is the same bits, not an approximation: a double times a normal power of two is exact while the product is a normal double, which holds for every exponent from -1022 to 960. A column outside that range sends the whole launch through the per-element form, as before.
The
[bl]line gains adeqfield (LH_DEQ_OUT), so the term is named from now on.NPU_DEQ_LDEXP=1keeps the old form for an A/B;NPU_DEQ_LDEXP=2runs both and counts differing elements.Measured
bonsai2-27b-t2s2, onenpu-holdticket (466 s), one binary, arms A B B A (A =NPU_DEQ_LDEXP=1), 103 16-row blocks per arm,NPU_PHASE_PROF=1.amdxdnaerror count 2561 -> 2561.The claim is the first pair: block median 630.4 -> 515.0 ms (25.4 -> 31.1 rows/s), TTFT 24.4-25.1 -> 29.8-30.6 tok/s.
The box slowed partway through B2 and stayed slow through A2 (device wait rose too, 212 -> 330 ms), so I do not quote the second pair as a speedup. The same thing happened in the second half of my two earlier tickets today; I have not established whether that is other lanes' load or something about a long ticket. Order was old-form first, so a warm-up or thermal drift would count against this change, not for it.
Bit identity on the device, same ticket (
NPU_DEQ_LDEXP=2):Greedy 8-token text sha256 is
d819d7c8ae824fb6in all five arms.Host microbench of the two forms alone (M=16, one plane / two planes):
One thing to know when reading the table: the measured binary also carried #2005's attention change, in every arm. This branch is that change removed and rebased onto main; I built it (
1bitlinks) and ran its unit test, but did not repeat the device run on this exact tree.Tests
tests/npu_pb16i_layout_test.cpp(already run by CI asnpu_pb16i_layout) gains an arm:dequant_rows()againstscale_i64()per element, compared as bit patterns, for one and two planes, container-like exponents, both edges of the table range, exponents below and above it, and sums that include 0, +-1 and the int32 extremes.Mutants: upper bound raised past the exact range -> FAIL;
ascalefolded into the double multiply -> FAIL; wrong column's power -> FAIL. Lowering the lower bound does not fail, and cannot: anything that small rounds to a float zero in both forms. The bound stays at -1022 because that is where the exactness argument holds.🤖 Generated with Claude Code