Files

122 lines
4.8 KiB
Go

// Copyright (C) 2026, Lux Industries Inc. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// kernel_test.go — the integer kernels' edges: the two guards that stop a degenerate
// input from dividing by zero (which on the consensus path panics a validator), the
// causal mask itself, and the saturation that keeps requantisation from wrapping.
package inference
import (
"math"
"testing"
"github.com/stretchr/testify/require"
)
func TestMaxu32(t *testing.T) {
for _, c := range [][2]uint32{{0, 0}, {3, 7}, {7, 3}, {5, 5}, {0, math.MaxUint32}, {math.MaxUint32, 0}} {
a, b := c[0], c[1]
got := maxu32(a, b)
require.GreaterOrEqual(t, got, a)
require.GreaterOrEqual(t, got, b)
require.True(t, got == a || got == b, "must return one of its arguments")
require.Equal(t, got, maxu32(b, a), "must be symmetric")
}
}
// TestRmsnormZeroRow: with eps 0 an all-zero activation row drives the RMS denominator
// to zero. The kernel must clamp it — an integer divide by zero panics, and a panic
// inside a precompile takes the validator down.
func TestRmsnormZeroRow(t *testing.T) {
const d = 4
out := make([]int8, d)
require.NotPanics(t, func() {
rmsnormI8(make([]int8, d), []int8{1, 1, 1, 1}, out, d, 5, 0)
})
require.Equal(t, make([]int8, d), out, "a zero row normalises to zeros")
}
// TestRmsnormDividesByTheRootMeanSquare pins the arithmetic itself: ss = 4·80² = 25600
// and isqrt(25600) = 160, so each output is (x·w·mult)/160.
func TestRmsnormDividesByTheRootMeanSquare(t *testing.T) {
const d = 4
x := []int8{80, 80, 80, 80}
out := make([]int8, d)
rmsnormI8(x, []int8{1, 1, 1, 1}, out, d, 8, 0)
require.Equal(t, []int8{4, 4, 4, 4}, out, "80·1·8/160")
rmsnormI8(x, []int8{2, 2, 2, 2}, out, d, 8, 0)
require.Equal(t, []int8{8, 8, 8, 8}, out, "doubling the weight doubles the output; the divisor is unchanged")
rmsnormI8(x, []int8{127, 127, 127, 127}, out, d, 8, 0)
require.Equal(t, []int8{127, 127, 127, 127}, out, "an over-range result saturates, it must never wrap")
// eps only enlarges the divisor, so raising it can only shrink the result.
prev := int8(math.MaxInt8)
for _, eps := range []int32{0, 39936, 1_000_000} {
rmsnormI8(x, []int8{1, 1, 1, 1}, out, d, 8, eps)
require.LessOrEqual(t, out[0], prev, "a larger eps must not enlarge the result")
prev = out[0]
}
require.Zero(t, prev, "a large enough eps drives the row to zero")
}
// TestAttentionAtLengthOne: with a single token there is exactly one key, so the
// softmax weight is 1 whatever the scores are. The output must therefore depend on v
// alone, and the causal flag must make no difference.
func TestAttentionAtLengthOne(t *testing.T) {
const seq, d = 1, 2
v := []int8{100, -100}
zeroScores := make([]int8, d)
loudScores := []int8{120, -7}
a := make([]int8, d)
b := make([]int8, d)
attentionI8(zeroScores, zeroScores, v, aivmExpLUT[:], a, seq, d, 9, 7, true)
attentionI8(loudScores, []int8{-60, 33}, v, aivmExpLUT[:], b, seq, d, 9, 7, true)
require.Equal(t, a, b, "at length one the scores cannot matter")
open := make([]int8, d)
attentionI8(zeroScores, zeroScores, v, aivmExpLUT[:], open, seq, d, 9, 7, false)
require.Equal(t, a, open, "at length one the mask has nothing to mask")
require.NotEqual(t, make([]int8, d), a, "the value vector must actually reach the output")
}
// TestAttentionMaskHidesTheFuture: under the causal mask row 0 is computed from token 0
// alone, so rewriting token 1's value must leave it untouched — and must not, with the
// mask off. Scores are all equal (q and k are zero) so any leak shows up in the output.
func TestAttentionMaskHidesTheFuture(t *testing.T) {
const seq, d = 2, 2
q := make([]int8, seq*d)
k := make([]int8, seq*d)
v := []int8{100, 100, -100, -100}
vFuture := []int8{100, 100, 7, 55} // same first row, different second
masked := make([]int8, seq*d)
maskedFuture := make([]int8, seq*d)
attentionI8(q, k, v, aivmExpLUT[:], masked, seq, d, 9, 7, true)
attentionI8(q, k, vFuture, aivmExpLUT[:], maskedFuture, seq, d, 9, 7, true)
require.Equal(t, masked[:d], maskedFuture[:d], "row 0 must not see row 1")
require.NotEqual(t, masked[d:], maskedFuture[d:], "row 1 must see row 1")
open := make([]int8, seq*d)
attentionI8(q, k, v, aivmExpLUT[:], open, seq, d, 9, 7, false)
require.NotEqual(t, masked[:d], open[:d], "with the mask off row 0 must see row 1")
}
// TestAttentionZeroTable: a probability table of all zeros drives the softmax
// denominator to zero. The kernel must clamp it rather than divide by zero.
func TestAttentionZeroTable(t *testing.T) {
const seq, d = 2, 2
out := make([]int8, seq*d)
require.NotPanics(t, func() {
attentionI8(make([]int8, seq*d), make([]int8, seq*d), []int8{100, 100, -100, -100},
make([]int32, 256), out, seq, d, 9, 7, true)
})
require.Equal(t, make([]int8, seq*d), out, "no probability mass, no output")
}