Skip to content

Commit 3fef1cb

Browse files
committed
test(metal): host float reference anchors the arch quant forward (#348's truth instrument)
hostArchQuantReference re-derives the arch decode forward on the host — production dequant for storage truth, then float64 rms/matvec/attention/gelu and the engine's own pinned RoPE — sharing NO emission or kernel code with the GPU lanes, so agreement is evidence of correctness rather than consistency (the ICB and stepToken lanes agree with each other on the 31B-geometry fixture while the real 31B degenerates). Anchored at cosine ≥ 0.999 per token across three geometries: uniform control, the 12B mix (global MQA kv=1) and the 31B mix (global GQA kv>1). All three pass — the BASIC quant forward is correct even at gkv>1, which together with the bf16-31B field run (degenerates identically to the 4-bit; the quant lane is exonerated end-to-end) narrows #348 to a FEATURE × gkv>1 interaction: qk-norm, the implicit value-norm, k_eq_v's V-from-K, or the proportional global rope — none of which the simple fixture exercises yet. The enrichment ladder continues from here. Instrument scar worth keeping: the shared fixture's ±1.0 weights explode the synthetic residual stream into the tens of thousands, where bf16's 2⁻⁷ step makes any two accumulation orders disagree violently — the conditioned builder (±0.1 weights) plus bf16-rounding the mirror at the GPU's residual stations is what makes the reference discriminate defects instead of fixture ill-conditioning. Co-Authored-By: Virgil <virgil@lethean.io>
1 parent b67e9cf commit 3fef1cb

1 file changed

Lines changed: 296 additions & 0 deletions

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
//go:build darwin && arm64
4+
5+
package native
6+
7+
import (
8+
"os"
9+
"testing"
10+
11+
"dappco.re/go/inference/model"
12+
)
13+
14+
// hostArchQuantReference is the independent float reference for the arch-driven quant decode
15+
// forward: the same weights (dequantised through the production dequantizeAffineRowsF32, so
16+
// storage truth is shared), the same per-layer geometry (kvHeadsOf/headDimOf), but every op —
17+
// rms, matvec, half-split RoPE, causal attention, gelu — re-derived on the host in float64.
18+
// It shares NO emission or kernel code with the GPU lanes, so agreement is evidence of
19+
// correctness rather than consistency (#348: the ICB and stepToken lanes agree with each
20+
// other on the 31B-geometry fixture while the real 31B degenerates — a truth anchor was the
21+
// missing instrument).
22+
func hostArchQuantReference(t *testing.T, inputs [][]byte, qlayers []QuantizedLayerWeights,
23+
specs []model.LayerSpec, dModel, nHeads, nKVHeads, headDim, dFF, window int,
24+
base, scale, eps float32) [][]byte {
25+
t.Helper()
26+
type mat struct {
27+
w []float32
28+
rows, cols int
29+
}
30+
deq := func(w QuantWeight, rows, cols int) mat {
31+
f, err := dequantizeAffineRowsF32(w.Packed, w.Scales, w.Biases, rows, cols, qlayers[0].GroupSize, qlayers[0].Bits)
32+
if err != nil {
33+
t.Fatalf("dequant: %v", err)
34+
}
35+
return mat{f, rows, cols}
36+
}
37+
matvec := func(m mat, x []float32) []float32 {
38+
out := make([]float32, m.rows)
39+
for r := range m.rows {
40+
var acc float64
41+
row := m.w[r*m.cols : (r+1)*m.cols]
42+
for c, v := range x {
43+
acc += float64(row[c]) * float64(v)
44+
}
45+
out[r] = float32(acc)
46+
}
47+
return out
48+
}
49+
roundBF16 := func(f []float32) {
50+
for i, v := range f {
51+
h := f32ToBF16(v)
52+
f[i] = bf16ToF32(byte(h), byte(h>>8))
53+
}
54+
}
55+
bf16ToF32s := func(b []byte) []float32 {
56+
f := make([]float32, len(b)/2)
57+
for i := range f {
58+
f[i] = bf16ToF32(b[i*2], b[i*2+1])
59+
}
60+
return f
61+
}
62+
// rotation via the engine's own host-callable RoPE (its convention is pinned by
63+
// rope_test's invariants) — the mirror's independence claim covers the LANE
64+
// composition (projection/cache/attention/residual order), not the rotation maths.
65+
rope := func(x []float32, heads, hd, pos int) {
66+
out, err := RoPE(x, 1, heads, hd, base, 1, pos, false)
67+
if err != nil {
68+
t.Fatalf("host RoPE: %v", err)
69+
}
70+
copy(x, out)
71+
}
72+
73+
type lw struct{ q, k, v, o, gate, up, down mat }
74+
ws := make([]lw, len(qlayers))
75+
for li, ql := range qlayers {
76+
lhd := headDimOf(specs[li], headDim)
77+
lkv := kvHeadsOf(specs[li], nKVHeads)
78+
ws[li] = lw{
79+
q: deq(ql.Q, nHeads*lhd, dModel), k: deq(ql.K, lkv*lhd, dModel), v: deq(ql.V, lkv*lhd, dModel),
80+
o: deq(ql.O, dModel, nHeads*lhd),
81+
gate: deq(ql.Gate, dFF, dModel), up: deq(ql.Up, dFF, dModel), down: deq(ql.Down, dModel, dFF),
82+
}
83+
}
84+
85+
kCache := make([][][]float32, len(qlayers)) // [layer][step][lkv*lhd]
86+
vCache := make([][][]float32, len(qlayers))
87+
outs := make([][]byte, len(inputs))
88+
perLayer := make([][][]byte, len(inputs)) // [tok][layer] hidden after the layer
89+
for tok := range inputs {
90+
x := bf16ToF32s(inputs[tok])
91+
for li := range qlayers {
92+
lhd := headDimOf(specs[li], headDim)
93+
lkv := kvHeadsOf(specs[li], nKVHeads)
94+
normed := rmsNormHostReference(x, bf16ToF32s(qlayers[li].AttnNormW), 1, dModel, eps)
95+
q := matvec(ws[li].q, normed)
96+
k := matvec(ws[li].k, normed)
97+
v := matvec(ws[li].v, normed)
98+
rope(q, nHeads, lhd, tok)
99+
rope(k, lkv, lhd, tok)
100+
kCache[li] = append(kCache[li], k)
101+
vCache[li] = append(vCache[li], v)
102+
first := 0
103+
if specs[li].Attention != model.GlobalAttention && window > 0 && len(kCache[li]) > window {
104+
first = len(kCache[li]) - window
105+
}
106+
n := len(kCache[li]) - first
107+
// head-major k/v bf16 buffers for sdpaBF16Reference: (hk·n + j)·lhd + d
108+
kb := make([]byte, lkv*n*lhd*bf16Size)
109+
vb := make([]byte, lkv*n*lhd*bf16Size)
110+
put := func(dst []byte, idx int, val float32) {
111+
h := f32ToBF16(val)
112+
dst[idx*2], dst[idx*2+1] = byte(h), byte(h>>8)
113+
}
114+
for j := range n {
115+
for hk := range lkv {
116+
for d := range lhd {
117+
put(kb, (hk*n+j)*lhd+d, kCache[li][first+j][hk*lhd+d])
118+
put(vb, (hk*n+j)*lhd+d, vCache[li][first+j][hk*lhd+d])
119+
}
120+
}
121+
}
122+
qb := make([]byte, nHeads*lhd*bf16Size)
123+
for i, qv := range q {
124+
put(qb, i, qv)
125+
}
126+
attn := bf16ToF32s(sdpaBF16Reference(qb, kb, vb, nHeads, lkv, lhd, n, scale))
127+
attnOut := matvec(ws[li].o, attn)
128+
for i := range x {
129+
x[i] += attnOut[i]
130+
}
131+
roundBF16(x)
132+
normed2 := rmsNormHostReference(x, bf16ToF32s(qlayers[li].MLPNormW), 1, dModel, eps)
133+
g := matvec(ws[li].gate, normed2)
134+
u := matvec(ws[li].up, normed2)
135+
for i := range g {
136+
g[i] = geluRefF32(g[i]) * u[i]
137+
}
138+
d := matvec(ws[li].down, g)
139+
for i := range x {
140+
x[i] += d[i]
141+
}
142+
roundBF16(x)
143+
perLayer[tok] = append(perLayer[tok], toBF16Bytes(x))
144+
}
145+
outs[tok] = toBF16Bytes(x)
146+
}
147+
hostPerLayerRef = perLayer
148+
return outs
149+
}
150+
151+
// hostPerLayerRef holds the host mirror's per-layer hiddens from the last
152+
// hostArchQuantReference run — the layer-bisect view for a diverging token.
153+
var hostPerLayerRef [][][]byte
154+
155+
// buildConditionedQuantLayer is buildQuantLayer at 10× smaller weight scale, so the
156+
// synthetic residual stream stays O(1) through the layers: the shared fixture's ±1.0
157+
// weights explode the hidden into the tens of thousands, where bf16's ~2⁻⁷ relative
158+
// step makes ANY two accumulation orders disagree violently and the reference cannot
159+
// discriminate a real defect from fixture ill-conditioning.
160+
func buildConditionedQuantLayer(t *testing.T, dModel, nHeads, nKV, headDim, dFF, gs, bits, salt int) QuantizedLayerWeights {
161+
t.Helper()
162+
qDim, kvDim := nHeads*headDim, nKV*headDim
163+
mk := func(n, s int) []float32 {
164+
f := make([]float32, n)
165+
for i := range f {
166+
f[i] = float32((i*s+7)%101-50) * 0.002
167+
}
168+
return f
169+
}
170+
return QuantizedLayerWeights{
171+
AttnNormW: toBF16Bytes(mk(dModel, salt+13)),
172+
MLPNormW: toBF16Bytes(mk(dModel, salt+19)),
173+
Q: quantW(t, mk(qDim*dModel, salt+53), qDim, dModel, gs, bits),
174+
K: quantW(t, mk(kvDim*dModel, salt+71), kvDim, dModel, gs, bits),
175+
V: quantW(t, mk(kvDim*dModel, salt+83), kvDim, dModel, gs, bits),
176+
O: quantW(t, mk(dModel*qDim, salt+17), dModel, qDim, gs, bits),
177+
Gate: quantW(t, mk(dFF*dModel, salt+61), dFF, dModel, gs, bits),
178+
Up: quantW(t, mk(dFF*dModel, salt+29), dFF, dModel, gs, bits),
179+
Down: quantW(t, mk(dModel*dFF, salt+47), dModel, dFF, gs, bits),
180+
GroupSize: gs, Bits: bits,
181+
}
182+
}
183+
184+
// TestDecodeForwardArchQuantHostReference anchors the GPU quant forward to the independent
185+
// host reference at BOTH non-uniform kv-head geometries: the 12B mix (global MQA, kv=1 —
186+
// the control every field-working model exercises) and the 31B mix (global GQA, kv>1 — the
187+
// geometry the degenerate 31B uniquely runs). Per-token hidden cosine ≥ 0.999; a lane that
188+
// is merely CONSISTENT with its sibling but wrong against the maths fails here.
189+
func TestDecodeForwardArchQuantHostReference(t *testing.T) {
190+
if os.Getenv(MetallibPathEnv) == "" {
191+
t.Skip("metallib not set")
192+
}
193+
const dModel, nHeads, headDim, globalHeadDim, dFF, gs, bits = 512, 8, 64, 128, 1024, 64, 4
194+
const base, scale, eps = float32(10000), float32(0.125), float32(1e-5)
195+
const maxLen, T, slidingWindow = 8, 6, 7
196+
197+
mkInputs := func(n, salt int) [][]byte {
198+
in := make([][]byte, n)
199+
for i := range in {
200+
f := make([]float32, dModel)
201+
for j := range f {
202+
f[j] = float32((j*(i+salt)+11)%83-41) * 0.02
203+
}
204+
in[i] = toBF16Bytes(f)
205+
}
206+
return in
207+
}
208+
209+
cases := []struct {
210+
name string
211+
slidingKV, globalKV int
212+
}{
213+
{"uniform-control", 2, 2},
214+
{"global-MQA-kv1-the-12B-mix", 2, 1},
215+
{"global-GQA-kv2-the-31B-mix", 4, 2},
216+
}
217+
for _, c := range cases {
218+
t.Run(c.name, func(t *testing.T) {
219+
specs := model.DeriveLayers([]string{"sliding_attention", "full_attention"}, 0)
220+
specs[0].KVHeads, specs[0].HeadDim = c.slidingKV, headDim
221+
ghd := globalHeadDim
222+
if c.name == "uniform-control" {
223+
ghd = headDim
224+
}
225+
specs[1].KVHeads, specs[1].HeadDim = c.globalKV, ghd
226+
ql := []QuantizedLayerWeights{
227+
buildConditionedQuantLayer(t, dModel, nHeads, c.slidingKV, headDim, dFF, gs, bits, 500),
228+
buildConditionedQuantLayer(t, dModel, nHeads, c.globalKV, ghd, dFF, gs, bits, 600),
229+
}
230+
inputs := mkInputs(T, 7)
231+
232+
got, err := DecodeForwardArchQuant(inputs, ql, specs, dModel, nHeads, c.slidingKV, headDim, maxLen, dFF, slidingWindow, base, scale, eps, false)
233+
if err != nil {
234+
t.Fatalf("DecodeForwardArchQuant: %v", err)
235+
}
236+
want := hostArchQuantReference(t, inputs, ql, specs, dModel, nHeads, c.slidingKV, headDim, dFF, slidingWindow, base, scale, eps)
237+
bad := -1
238+
for tok := range T {
239+
cos := cosineBF16(got[tok], want[tok])
240+
t.Logf("tok %d cosine=%.6f", tok, cos)
241+
if bad < 0 && cos < 0.999 {
242+
bad = tok
243+
}
244+
}
245+
if bad >= 0 {
246+
g, w := got[bad], want[bad]
247+
type d struct {
248+
i int
249+
gv, wv, ad float64
250+
}
251+
var worst []d
252+
for i := 0; i*2 < len(g); i++ {
253+
gv := float64(bf16ToF32(g[i*2], g[i*2+1]))
254+
wv := float64(bf16ToF32(w[i*2], w[i*2+1]))
255+
ad := gv - wv
256+
if ad < 0 {
257+
ad = -ad
258+
}
259+
worst = append(worst, d{i, gv, wv, ad})
260+
}
261+
for a := range worst {
262+
for b := a + 1; b < len(worst); b++ {
263+
if worst[b].ad > worst[a].ad {
264+
worst[a], worst[b] = worst[b], worst[a]
265+
}
266+
}
267+
if a >= 5 {
268+
break
269+
}
270+
}
271+
for _, e := range worst[:6] {
272+
t.Logf("tok %d elem %d: gpu=%.3f host=%.3f |d|=%.3f", bad, e.i, e.gv, e.wv, e.ad)
273+
}
274+
}
275+
if bad >= 0 {
276+
capturedLayerHiddens = nil
277+
captureLayerHiddens = true
278+
_, rerr := DecodeForwardArchQuant(inputs, ql, specs, dModel, nHeads, c.slidingKV, headDim, maxLen, dFF, slidingWindow, base, scale, eps, false)
279+
captureLayerHiddens = false
280+
if rerr != nil {
281+
t.Fatalf("capture rerun: %v", rerr)
282+
}
283+
nL := len(ql)
284+
for tok := range T {
285+
for li := range nL {
286+
gpu := capturedLayerHiddens[tok*nL+li]
287+
host := hostPerLayerRef[tok][li]
288+
t.Logf("tok %d layer %d (%v) cosine=%.6f", tok, li, specs[li].Attention, cosineBF16(gpu, host))
289+
}
290+
}
291+
t.Fatalf("GPU vs host reference diverges at %s from tok %d", c.name, bad)
292+
}
293+
t.Logf("%s: %d tokens GPU ≡ host reference (cosine ≥ 0.999)", c.name, T)
294+
})
295+
}
296+
}

0 commit comments

Comments
 (0)