mirror of
https://github.com/luxfi/precompile.git
synced 2026-09-24 00:26:21 +00:00
122 lines
4.8 KiB
Go
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")
|
|
}
|