Skip to content

Commit f835aea

Browse files
committed
model/needle: tests — oracle parity, unit maths, tokenizer receipts
Proves the reference against a PyTorch oracle (modeling_needle.py + real weights) and unit-checks every primitive: - Generate reproduces the exact coherent call from the real weights, plus two further tool-use prompts (generalisation, not a memorised fixture). - Exact encoder-input + generated token parity; encoder-hidden and decoder cross-attention-step numerics matched to the oracle; first-token top-5 logit parity (ids + magnitudes) — the gold receipt. - ZCRMSNorm formula (incl. no-mean-centring guard), NeoX RoPE, GQA linear algebra, bf16 widen, and SentencePiece encode/decode against oracle ids. Real-weight tests skip cleanly when the checkpoint is absent (CI stays green). Co-Authored-By: Virgil <virgil@lethean.io>
1 parent 09fd8b1 commit f835aea

8 files changed

Lines changed: 532 additions & 0 deletions

File tree

go/model/needle/config_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package needle
4+
5+
import "testing"
6+
7+
// TestConfig_DefaultConfig_Dims pins the published Needle geometry the reference
8+
// depends on: 512-d, 8 query / 4 kv heads (head_dim 64), 12 encoder / 8 decoder
9+
// layers, vocab 8192.
10+
func TestConfig_DefaultConfig_Dims(t *testing.T) {
11+
c := DefaultConfig()
12+
if c.HeadDim() != 64 {
13+
t.Errorf("HeadDim = %d, want 64", c.HeadDim())
14+
}
15+
if c.KVDim() != 256 {
16+
t.Errorf("KVDim = %d, want 256", c.KVDim())
17+
}
18+
if c.NumEncoderLayers != 12 || c.NumDecoderLayers != 8 {
19+
t.Errorf("layers = %d/%d, want 12/8", c.NumEncoderLayers, c.NumDecoderLayers)
20+
}
21+
if c.EosTokenID != 1 || c.ToolsTokenID != 5 {
22+
t.Errorf("special ids eos=%d tools=%d, want 1/5", c.EosTokenID, c.ToolsTokenID)
23+
}
24+
}
25+
26+
// TestConfig_LoadConfig_RealCheckpoint reads the real config.json and confirms it
27+
// matches the published geometry (and that a present file overrides defaults).
28+
func TestConfig_LoadConfig_RealCheckpoint(t *testing.T) {
29+
cfg, err := LoadConfig(snapshotDir)
30+
if err != nil {
31+
t.Fatalf("LoadConfig: %v", err)
32+
}
33+
if cfg.HiddenSize != 512 || cfg.VocabSize != 8192 {
34+
t.Errorf("hidden/vocab = %d/%d, want 512/8192", cfg.HiddenSize, cfg.VocabSize)
35+
}
36+
if cfg.NumKVHeads != 4 || cfg.NumHeads != 8 {
37+
t.Errorf("heads = %d/%d, want 8/4", cfg.NumHeads, cfg.NumKVHeads)
38+
}
39+
}
40+
41+
// TestConfig_LoadConfig_MissingIsDefault confirms a directory without config.json
42+
// yields DefaultConfig rather than an error — a bare weights+tokenizer pack runs.
43+
func TestConfig_LoadConfig_MissingIsDefault(t *testing.T) {
44+
cfg, err := LoadConfig(t.TempDir())
45+
if err != nil {
46+
t.Fatalf("LoadConfig(empty dir): %v", err)
47+
}
48+
if cfg != DefaultConfig() {
49+
t.Errorf("missing config.json did not fall back to DefaultConfig")
50+
}
51+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package needle
4+
5+
import "testing"
6+
7+
// TestGenerate_Generalises runs the reference on tool-use prompts beyond the
8+
// primary fixture, each checked against its PyTorch-oracle output. Matching more
9+
// than one prompt shows the port is the architecture, not a memorised fixture.
10+
func TestGenerate_Generalises(t *testing.T) {
11+
m := loadTestModel(t)
12+
cases := []struct {
13+
name, query, tools, want string
14+
}{
15+
{
16+
name: "send_email",
17+
query: "Send an email to john@example.com saying hello",
18+
tools: `[{"name":"send_email","description":"Send an email to a recipient.","parameters":{"to":{"type":"string","description":"The recipient email address.","required":true},"body":{"type":"string","description":"The email body text.","required":true}}}]`,
19+
want: ` [{"name":"send_email","arguments":{"to":"john@example.com","body":"hello"}}]`,
20+
},
21+
{
22+
name: "get_stock_price",
23+
query: "Get the current stock price of AAPL",
24+
tools: `[{"name":"get_stock_price","description":"Get the current stock price.","parameters":{"symbol":{"type":"string","description":"Ticker symbol.","required":true}}}]`,
25+
want: ` [{"name":"get_stock_price","arguments":{"symbol":"AAPL"}}]`,
26+
},
27+
}
28+
for _, tc := range cases {
29+
t.Run(tc.name, func(t *testing.T) {
30+
if got := m.Generate(tc.query, tc.tools, 64); got != tc.want {
31+
t.Fatalf("Generate(%s):\n got: %q\nwant: %q", tc.name, got, tc.want)
32+
}
33+
})
34+
}
35+
}
36+
37+
// TestGenerate_FirstTokenLogitParity is the gold oracle receipt: the reference's
38+
// first-step logits (decoder over [EOS]) must match the PyTorch oracle in both
39+
// the top-5 token ranking and the raw logit magnitudes (within f32-vs-bf16
40+
// tolerance), not merely the argmax.
41+
func TestGenerate_FirstTokenLogitParity(t *testing.T) {
42+
m := loadTestModel(t)
43+
encoderHidden := m.encode(oracleEncIDs)
44+
dh := m.decode([]int{m.cfg.EosTokenID}, encoderHidden, len(oracleEncIDs))
45+
logits := m.logits(dh[:m.cfg.HiddenSize])
46+
47+
wantIDs := []int{4, 294, 8063, 302, 264}
48+
wantVals := []float32{1.22699, -14.80001, -16.13503, -17.17564, -17.1861}
49+
50+
gotIDs, gotVals := top5(logits)
51+
for i := range wantIDs {
52+
if gotIDs[i] != wantIDs[i] {
53+
t.Errorf("top5 id[%d] = %d, want %d (full got %v)", i, gotIDs[i], wantIDs[i], gotIDs)
54+
}
55+
if !close32(gotVals[i], wantVals[i], 0.05) {
56+
t.Errorf("top5 logit[%d] = %.5f, want %.5f", i, gotVals[i], wantVals[i])
57+
}
58+
}
59+
}
60+
61+
// top5 returns the five highest-scoring ids and their logits, descending.
62+
func top5(v []float32) ([]int, []float32) {
63+
ids := make([]int, 5)
64+
vals := make([]float32, 5)
65+
for i := range 5 {
66+
vals[i] = -1e30
67+
}
68+
for id, x := range v {
69+
for r := range 5 {
70+
if x > vals[r] {
71+
copy(ids[r+1:], ids[r:4])
72+
copy(vals[r+1:], vals[r:4])
73+
ids[r] = id
74+
vals[r] = x
75+
break
76+
}
77+
}
78+
}
79+
return ids, vals
80+
}

go/model/needle/linalg_test.go

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package needle
4+
5+
import (
6+
"math"
7+
"testing"
8+
)
9+
10+
// TestLinalg_linearNoBias_RowDot confirms y = x . Wᵀ: output o is x dotted with
11+
// weight row o (row-major [outDim, inDim]).
12+
func TestLinalg_linearNoBias_RowDot(t *testing.T) {
13+
x := []float32{1, 2, 3}
14+
// W = [[1,0,0],[0,1,0],[1,1,1]] flattened row-major.
15+
w := []float32{1, 0, 0, 0, 1, 0, 1, 1, 1}
16+
got := linearNoBias(x, w, 3, 3)
17+
want := []float32{1, 2, 6}
18+
for i := range want {
19+
if got[i] != want[i] {
20+
t.Errorf("linearNoBias[%d] = %v, want %v", i, got[i], want[i])
21+
}
22+
}
23+
}
24+
25+
// TestLinalg_softmaxInPlace_Distribution confirms softmax sums to 1 and is
26+
// monotonic in the inputs.
27+
func TestLinalg_softmaxInPlace_Distribution(t *testing.T) {
28+
s := []float32{1, 2, 3}
29+
softmaxInPlace(s)
30+
var sum float32
31+
for _, v := range s {
32+
sum += v
33+
}
34+
if math.Abs(float64(sum-1)) > 1e-6 {
35+
t.Errorf("softmax sum = %v, want 1", sum)
36+
}
37+
if !(s[0] < s[1] && s[1] < s[2]) {
38+
t.Errorf("softmax not monotonic: %v", s)
39+
}
40+
}
41+
42+
// TestLinalg_sigmoid_Midpoint confirms sigmoid(0) = 0.5, the init value of every
43+
// residual gate (weight init 0 -> half-open gate).
44+
func TestLinalg_sigmoid_Midpoint(t *testing.T) {
45+
if got := sigmoid(0); math.Abs(float64(got-0.5)) > 1e-6 {
46+
t.Errorf("sigmoid(0) = %v, want 0.5", got)
47+
}
48+
}
49+
50+
// TestLinalg_clipAdd_Clamps confirms the bf16-range clamp mirrors _add_clipped.
51+
func TestLinalg_clipAdd_Clamps(t *testing.T) {
52+
if got := clipAdd(65000, 1000); got != 65500 {
53+
t.Errorf("clipAdd over-range = %v, want 65500", got)
54+
}
55+
if got := clipAdd(-65000, -1000); got != -65500 {
56+
t.Errorf("clipAdd under-range = %v, want -65500", got)
57+
}
58+
if got := clipAdd(1.5, 2.5); got != 4 {
59+
t.Errorf("clipAdd in-range = %v, want 4", got)
60+
}
61+
}

go/model/needle/model_test.go

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package needle
4+
5+
import (
6+
"math"
7+
"slices"
8+
"testing"
9+
10+
coreio "dappco.re/go/io"
11+
)
12+
13+
// snapshotDir is the local Needle checkpoint. Tests that need the real weights
14+
// skip when it is absent (e.g. CI), so the suite stays green without the model
15+
// while giving a real end-to-end receipt on a machine that has it.
16+
const snapshotDir = "/Users/snider/.cache/huggingface/hub/models--Cactus-Compute--needle/snapshots/5f89b4307696d669c3df1d38ae057e6e1728b107"
17+
18+
// oracleQuery/oracleTools are the exact prompt the PyTorch reference was run on;
19+
// the fixtures below are that run's output (see the lane report).
20+
const (
21+
oracleQuery = "What is the weather in San Francisco?"
22+
oracleTools = `[{"name":"get_weather","description":"Get current weather for a city.","parameters":{"location":{"type":"string","description":"City name.","required":true}}}]`
23+
)
24+
25+
var (
26+
oracleEncIDs = []int{4279, 743, 302, 1149, 362, 711, 327, 1295, 1075, 378, 275, 8047, 8105, 5, 356, 294, 264, 358, 8062, 1331, 265, 283, 264, 618, 407, 1149, 345, 289, 2082, 284, 318, 282, 506, 282, 298, 264, 315, 265, 283, 264, 2523, 417, 284, 301, 262, 312, 434}
27+
oracleGenIDs = []int{4, 356, 294, 264, 358, 8062, 1331, 265, 393, 282, 506, 264, 8074, 327, 1295, 1075, 378, 275, 8047, 503}
28+
oracleOutput = ` [{"name":"get_weather","arguments":{"location":"San Francisco"}}]`
29+
)
30+
31+
func loadTestModel(t *testing.T) *Model {
32+
t.Helper()
33+
if !coreio.Local.Exists(snapshotDir + "/model.safetensors") {
34+
t.Skip("needle checkpoint not present at " + snapshotDir)
35+
}
36+
m, err := Load(snapshotDir)
37+
if err != nil {
38+
t.Fatalf("Load: %v", err)
39+
}
40+
return m
41+
}
42+
43+
func close32(a, b, tol float32) bool { return float32(math.Abs(float64(a-b))) <= tol }
44+
45+
// TestModel_Generate_ToolCall is the headline receipt: from the real weights, the
46+
// reference greedily generates the exact coherent function call the PyTorch
47+
// oracle produced.
48+
func TestModel_Generate_ToolCall(t *testing.T) {
49+
m := loadTestModel(t)
50+
got := m.Generate(oracleQuery, oracleTools, 64)
51+
if got != oracleOutput {
52+
t.Fatalf("Generate mismatch:\n got: %q\nwant: %q", got, oracleOutput)
53+
}
54+
t.Logf("VERBATIM GENERATION: %s", got)
55+
}
56+
57+
// TestModel_generateIDs_TokenParity pins exact encoder-input and generated token
58+
// ids against the oracle — a stricter check than the decoded string.
59+
func TestModel_generateIDs_TokenParity(t *testing.T) {
60+
m := loadTestModel(t)
61+
enc, gen := m.generateIDs(oracleQuery, oracleTools, 64)
62+
if !slices.Equal(enc, oracleEncIDs) {
63+
t.Fatalf("encoder ids mismatch:\n got: %v\nwant: %v", enc, oracleEncIDs)
64+
}
65+
if !slices.Equal(gen, oracleGenIDs) {
66+
t.Fatalf("generated ids mismatch:\n got: %v\nwant: %v", gen, oracleGenIDs)
67+
}
68+
}
69+
70+
// TestModel_encode_EncoderHidden checks the encoder pass numerically against the
71+
// oracle's encoder output (first token, last token, and the whole-tensor sum),
72+
// isolating encoder correctness from the decoder.
73+
func TestModel_encode_EncoderHidden(t *testing.T) {
74+
m := loadTestModel(t)
75+
h := m.encode(oracleEncIDs)
76+
if len(h) != len(oracleEncIDs)*m.cfg.HiddenSize {
77+
t.Fatalf("encoder hidden len = %d, want %d", len(h), len(oracleEncIDs)*m.cfg.HiddenSize)
78+
}
79+
wantT0 := []float32{-1.44818, 0.02975, 0.43704, 0.51372, -1.47645}
80+
wantTlast := []float32{-0.62455, 0.66252, 0.34196, 0.43071, 0.47612}
81+
hidden := m.cfg.HiddenSize
82+
last := (len(oracleEncIDs) - 1) * hidden
83+
for i := range wantT0 {
84+
if !close32(h[i], wantT0[i], 0.02) {
85+
t.Errorf("enc_hid[0][%d] = %.5f, want %.5f", i, h[i], wantT0[i])
86+
}
87+
if !close32(h[last+i], wantTlast[i], 0.02) {
88+
t.Errorf("enc_hid[last][%d] = %.5f, want %.5f", i, h[last+i], wantTlast[i])
89+
}
90+
}
91+
var sum float32
92+
for _, v := range h {
93+
sum += v
94+
}
95+
if !close32(sum, -730.317, 2.0) {
96+
t.Errorf("encoder hidden sum = %.3f, want ~-730.317", sum)
97+
}
98+
}
99+
100+
// TestModel_decode_CrossAttentionStep checks the decoder-with-cross-attention
101+
// step: the decoder hidden (post-norm) after one step over [EOS], against the
102+
// oracle. This exercises masked self-attn + cross-attn + gates end to end.
103+
func TestModel_decode_CrossAttentionStep(t *testing.T) {
104+
m := loadTestModel(t)
105+
encoderHidden := m.encode(oracleEncIDs)
106+
dh := m.decode([]int{m.cfg.EosTokenID}, encoderHidden, len(oracleEncIDs))
107+
want := []float32{-0.01168, 0.01785, -0.14802, -0.12168, 0.00175}
108+
for i := range want {
109+
if !close32(dh[i], want[i], 0.02) {
110+
t.Errorf("dec_hid[step0][%d] = %.5f, want %.5f", i, dh[i], want[i])
111+
}
112+
}
113+
// First-token argmax must be <tool_call> (id 4).
114+
if got := argmax(m.logits(dh[:m.cfg.HiddenSize])); got != 4 {
115+
t.Errorf("first-token argmax = %d, want 4 (<tool_call>)", got)
116+
}
117+
}

go/model/needle/norm_test.go

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// SPDX-Licence-Identifier: EUPL-1.2
2+
3+
package needle
4+
5+
import (
6+
"math"
7+
"testing"
8+
)
9+
10+
// TestNorm_zcRMSNorm_Formula pins the exact ZCRMSNorm maths: variance is the mean
11+
// of squares (NOT centred), and the weight enters as (1 + weight). x = [1,2,3,4],
12+
// weight = 0 -> x / rms(x), rms = sqrt(30/4).
13+
func TestNorm_zcRMSNorm_Formula(t *testing.T) {
14+
x := []float32{1, 2, 3, 4}
15+
weight := []float32{0, 0, 0, 0}
16+
inv := float32(1.0 / math.Sqrt(7.5+1e-6))
17+
want := []float32{1 * inv, 2 * inv, 3 * inv, 4 * inv}
18+
got := zcRMSNorm(x, weight, 1e-6)
19+
for i := range want {
20+
if math.Abs(float64(got[i]-want[i])) > 1e-5 {
21+
t.Errorf("zcRMSNorm[%d] = %.6f, want %.6f", i, got[i], want[i])
22+
}
23+
}
24+
}
25+
26+
// TestNorm_zcRMSNorm_WeightShift confirms the (1 + weight) scale: a weight of 1
27+
// doubles the normalised output, a weight of 0 leaves it, matching the
28+
// zero-centred convention (init-0 weight => identity scale).
29+
func TestNorm_zcRMSNorm_WeightShift(t *testing.T) {
30+
x := []float32{2, 0, 0, 0} // rms = sqrt(4/4) = 1, so normalised == x
31+
base := zcRMSNorm(x, []float32{0, 0, 0, 0}, 1e-6)
32+
if math.Abs(float64(base[0]-2)) > 1e-4 {
33+
t.Fatalf("weight 0: got %.6f, want 2", base[0])
34+
}
35+
shifted := zcRMSNorm(x, []float32{1, 0, 0, 0}, 1e-6)
36+
if math.Abs(float64(shifted[0]-4)) > 1e-4 {
37+
t.Fatalf("weight 1: got %.6f, want 4 (2 * (1+1))", shifted[0])
38+
}
39+
}
40+
41+
// TestNorm_zcRMSNorm_NotMeanCentred guards the classic mistake: if the mean were
42+
// subtracted, a constant vector would normalise to zero. ZCRMSNorm must NOT do
43+
// that — a constant vector normalises to +-1 * (1+weight).
44+
func TestNorm_zcRMSNorm_NotMeanCentred(t *testing.T) {
45+
got := zcRMSNorm([]float32{3, 3, 3, 3}, []float32{0, 0, 0, 0}, 1e-6)
46+
for i, v := range got {
47+
if math.Abs(float64(v-1)) > 1e-4 {
48+
t.Errorf("constant-vector norm[%d] = %.6f, want 1 (mean must NOT be subtracted)", i, v)
49+
}
50+
}
51+
}

0 commit comments

Comments
 (0)