diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 210bfaacb..0e2ee4c36 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,6 +41,15 @@ jobs: tasks: verifyNpmPins jsTest wasmJsTest wasmWasiTest - name: native tasks: linuxX64Test + # golden-parity: the SKEEP-003 packed-encoding gate (#1005). Bit-identical decode / + # scalar-kernel / TurboQuant digests on JVM and Kotlin/Native plus the dispatch parity + # tests, and the binary-compatibility check (apiCheck) that the contributing docs require + # but the per-target test legs do not run. Fast (a few minutes) so it fails early. + - name: golden-parity + tasks: >- + apiCheck + :skainet-backends:skainet-backend-cpu:jvmTest --tests sk.ainet.exec.golden.* + :skainet-backends:skainet-backend-cpu:linuxX64Test --tests sk.ainet.exec.golden.* permissions: contents: read diff --git a/docs/modules/ROOT/pages/contributing/build-from-source.adoc b/docs/modules/ROOT/pages/contributing/build-from-source.adoc index ebe3cd226..9e743f5bc 100644 --- a/docs/modules/ROOT/pages/contributing/build-from-source.adoc +++ b/docs/modules/ROOT/pages/contributing/build-from-source.adoc @@ -165,6 +165,33 @@ Applying `sk.ainet.multiplatform` to the *root* project is not supported and fai . Rewrite any `val someSourceSet by getting ++{++ ++}++` to the convention accessors (`jvmMain ++{++ ++}++`, `iosArm64Main ++{++ ++}++`). The `by getting` form resolves eagerly and fails now that targets are created by the plugin. . Confirm nothing moved: `./gradlew :module:tasks --all` before and after must be identical, and `./gradlew apiCheck` must pass without a new dump. +=== Pre-PR Gate and the Packed-Encoding Golden Parity Tests + +CI runs the test legs per target (`jvmTest`, `jsTest wasmJsTest wasmWasiTest`, `linuxX64Test`, +`assemble`) and, since the SKEEP-003 roadmap, a `golden-parity` leg. Before opening a PR, run the +same set locally: + +[source,bash] +---- +scripts/pr-gate.sh # full gate: jvmTest, apiCheck, JS/Wasm tests, linuxX64Test, assemble, Java API tests +scripts/pr-gate.sh --quick # jvmTest + apiCheck only, for iteration +scripts/pr-gate.sh --golden # packed-encoding golden parity (JVM + linuxX64) + apiCheck only +scripts/pr-gate.sh --bench # full gate + StorageBenchmarks and JMH microbenchmarks +---- + +The script points `CHROME_BIN` at an installed Chrome/Chromium for the Karma browser tests and +expects a JDK 21+ (`JAVA_HOME`; CI uses 25). + +*Golden parity tests* (`skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/`) +guard the packed encodings bit-for-bit: for every GGML block format (Q4_0, Q5_0, Q5_1, Q8_0, Q4_K, +Q5_K, Q6_K), ternary (TQ2_0) and TurboQuant they decode seeded bytes, run the scalar reference +kernels and encode/decode seeded vectors, and compare an FNV-64 digest of the raw float bits against +the values recorded in `Goldens.kt`. They are compiled into the JVM and Kotlin/Native test targets +(JS/Wasm compute `Float` in double precision and are covered by the tolerance-based +`PackedMatmulDispatchParityTest` in `commonTest` instead). A mismatch means a decoder or kernel no +longer produces the same bits; if that is the intent of a PR, re-baseline `Goldens.kt` in the same +PR and say so. See SKEEP-003 and the memory-architecture design record under `docs/design/memory/`. + === Pinning npm Packages Kotlin/JS and Kotlin/Wasm dependencies are locked in `kotlin-js-store/yarn.lock` and `kotlin-js-store/wasm/yarn.lock`. Both files are *generated*. Editing them by hand does not survive the next lockfile refresh — see https://github.com/SKaiNET-developers/SKaiNET/pull/894[PR #894], where a hand-applied `ws` security bump was reverted by `kotlinWasmUpgradeYarnLock` in the same pull request. diff --git a/scripts/pr-gate.sh b/scripts/pr-gate.sh index 616b88670..67abe64c5 100755 --- a/scripts/pr-gate.sh +++ b/scripts/pr-gate.sh @@ -6,6 +6,7 @@ # scripts/pr-gate.sh # full gate # scripts/pr-gate.sh --bench # full gate + StorageBenchmarks and JMH microbenchmarks # scripts/pr-gate.sh --quick # JVM leg + apiCheck only (iterate fast, then run the full gate) +# scripts/pr-gate.sh --golden # packed-encoding golden parity (JVM + linuxX64) + apiCheck only # # Set JAVA_HOME to a JDK 25 (CI uses 25; the build requires >= 21). set -euo pipefail @@ -25,6 +26,14 @@ echo "pr-gate: JAVA_HOME=${JAVA_HOME:-} CHROME_BIN=${CHROME_BIN:-= 31) return sign or 0x7C00 + return sign or (expo shl 10) or (mant ushr 13) + } + + private fun le16(b: ByteArray, off: Int, h: Int) { b[off] = (h and 0xFF).toByte(); b[off + 1] = ((h ushr 8) and 0xFF).toByte() } + + /** Valid random block: random payload, sane FP16 scales (no NaN/Inf). */ + private fun block(f: Fmt, rng: Rng): ByteArray { + val b = ByteArray(f.bytesPerBlock) { rng.nextByte() } + val d = rng.nextFloat() * 0.045f + 0.005f; val dMin = rng.nextFloat() * 0.02f + 0.005f; val m = rng.nextFloat() - 0.5f + when (f) { + Fmt.Q4_0, Fmt.Q5_0, Fmt.Q8_0 -> le16(b, 0, half(d)) + Fmt.Q5_1 -> { le16(b, 0, half(d)); le16(b, 2, half(m)) } + Fmt.Q4_K, Fmt.Q5_K -> { le16(b, 0, half(d)); le16(b, 2, half(dMin)) } + Fmt.Q6_K -> le16(b, 208, half(d)) + } + return b + } + + private val ctx = DirectCpuExecutionContext() + + @Suppress("UNCHECKED_CAST") + private fun parity(f: Fmt, build: (Shape, ByteArray) -> PackedBlockStorage) { + val outDim = 4; val blocksPerRow = 3; val inDim = blocksPerRow * f.blockSize; val batch = 3 + val rng = Rng(0x5EED_0004L + f.ordinal) + // canonical row-major bytes: row o's blocks contiguous + val bytes = ByteArray(outDim * blocksPerRow * f.bytesPerBlock) + for (o in 0 until outDim) for (bI in 0 until blocksPerRow) { + block(f, rng).copyInto(bytes, (o * blocksPerRow + bI) * f.bytesPerBlock) + } + val storage = build(Shape(outDim, inDim), bytes) + val w = ctx.fromData(storage as TensorData, FP32::class) + val xf = FloatArray(batch * inDim) { rng.nextFloat() * 2f - 1f } + val x = ctx.fromFloatArray(Shape(batch, inDim), FP32::class, xf) + + val actual = ctx.ops.matmul(x, ctx.ops.transpose(w)).data.copyToFloatArray() + + // FP32 reference from the decoded weight + val wf = FloatArray(outDim * inDim); val tmp = FloatArray(f.blockSize) + for (b in 0 until outDim * blocksPerRow) { storage.dequantizeBlock(b, tmp, 0); tmp.copyInto(wf, b * f.blockSize) } + val expected = FloatArray(batch * outDim) + var maxRef = 0f + for (r in 0 until batch) for (o in 0 until outDim) { + var acc = 0f + for (i in 0 until inDim) acc += xf[r * inDim + i] * wf[o * inDim + i] + expected[r * outDim + o] = acc; maxRef = maxOf(maxRef, abs(acc)) + } + val tol = 5e-3f * maxOf(maxRef, 1f) + 1e-4f + for (i in expected.indices) { + assertTrue( + abs(expected[i] - actual[i]) <= tol, + "${f.name}: dispatch output[$i]=${actual[i]} vs reference ${expected[i]} (tol $tol)", + ) + } + } + + @Test fun q4_0() = parity(Fmt.Q4_0) { s, b -> Q4_0BlockTensorData(s, b) } + @Test fun q5_0() = parity(Fmt.Q5_0) { s, b -> Q5_0BlockTensorData(s, b) } + @Test fun q5_1() = parity(Fmt.Q5_1) { s, b -> Q5_1BlockTensorData(s, b) } + @Test fun q8_0() = parity(Fmt.Q8_0) { s, b -> Q8_0BlockTensorData(s, b) } + @Test fun q4_K() = parity(Fmt.Q4_K) { s, b -> Q4_KBlockTensorData(s, b) } + @Test fun q5_K() = parity(Fmt.Q5_K) { s, b -> Q5_KBlockTensorData(s, b) } + @Test fun q6_K() = parity(Fmt.Q6_K) { s, b -> Q6_KBlockTensorData(s, b) } +} diff --git a/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/GoldenSupport.kt b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/GoldenSupport.kt new file mode 100644 index 000000000..02eabc8b0 --- /dev/null +++ b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/GoldenSupport.kt @@ -0,0 +1,137 @@ +package sk.ainet.exec.golden + +import kotlin.test.fail + +/** + * Support for the SKEEP-003 golden parity gate: deterministic inputs for every packed encoding + * and a compact, bit-exact digest of the results. + * + * The digest is an FNV-1a 64-bit hash over the raw IEEE-754 bits of the produced floats (or the + * produced bytes), plus the first four raw float bit patterns for a readable diff. A digest is + * stable for the same bytes on every strict-binary32 target (JVM, Kotlin/Native); it changes the + * moment a decoder or scalar kernel produces a different bit anywhere. To re-baseline after an + * *intended* numeric change, run the tests and copy the "actual" value into [Goldens]. + */ +internal object GoldenSupport { + + /** xorshift64* — small, allocation-free, identical on every target. */ + class Rng(seed: Long) { + private var s: Long = if (seed == 0L) 0x9E3779B97F4A7C15uL.toLong() else seed + fun nextLong(): Long { + var x = s + x = x xor (x ushr 12); x = x xor (x shl 25); x = x xor (x ushr 27) + s = x + return x * 0x2545F4914F6CDD1DuL.toLong() + } + fun nextByte(): Byte = (nextLong() ushr 56).toByte() + /** Uniform in [0, 1) with 24 bits of randomness (exactly representable as Float). */ + fun nextFloat(): Float = ((nextLong() ushr 40).toInt() and 0xFFFFFF) / 16777216.0f + /** Uniform in [-1, 1). */ + fun nextSigned(): Float = nextFloat() * 2f - 1f + } + + /** Round-to-nearest-even FP32 → FP16 bits (same routine as the kernel parity tests). */ + fun half(v: Float): Int { + val bits = v.toRawBits() + val sign = (bits ushr 16) and 0x8000 + val expo = ((bits ushr 23) and 0xFF) - 127 + 15 + val mant = bits and 0x7FFFFF + if (expo <= 0) return sign + if (expo >= 31) return sign or 0x7C00 + return sign or (expo shl 10) or (mant ushr 13) + } + + fun le16(b: ByteArray, off: Int, h: Int) { + b[off] = (h and 0xFF).toByte(); b[off + 1] = ((h ushr 8) and 0xFF).toByte() + } + + /** One packed GGML encoding: block geometry and a seeded block builder. */ + enum class Packed(val blockSize: Int, val bytesPerBlock: Int) { + Q4_0(32, 18), Q5_0(32, 22), Q5_1(32, 24), Q8_0(32, 34), Q4_K(256, 144), Q5_K(256, 176), Q6_K(256, 210); + + /** + * A random but *valid* block: every byte random, then the FP16 fields overwritten with sane + * scales so no NaN/Inf can enter the arithmetic (NaN payloads are not portable). + */ + fun block(rng: Rng): ByteArray { + val b = ByteArray(bytesPerBlock) { rng.nextByte() } + val d = rng.nextFloat() * 0.045f + 0.005f + val dMin = rng.nextFloat() * 0.02f + 0.005f + val m = rng.nextSigned() * 0.5f + when (this) { + Q4_0, Q5_0, Q8_0 -> le16(b, 0, half(d)) + Q5_1 -> { le16(b, 0, half(d)); le16(b, 2, half(m)) } + Q4_K, Q5_K -> { le16(b, 0, half(d)); le16(b, 2, half(dMin)) } + Q6_K -> le16(b, 208, half(d)) + } + return b + } + } + + /** Seeded weight: [rows] output rows × [blocksPerRow] blocks, returned per (row, block). */ + fun weightBlocks(p: Packed, rows: Int, blocksPerRow: Int, seed: Long): Array> { + val rng = Rng(seed) + return Array(rows) { Array(blocksPerRow) { p.block(rng) } } + } + + /** Canonical row-major bytes ([out, in] tensor on disk / in a TensorData). */ + fun rowMajor(blocks: Array>): ByteArray { + val out = ArrayList() + for (row in blocks) for (blk in row) for (x in blk) out.add(x) + return out.toByteArray() + } + + /** Block-major bytes (the scalar kernels' input layout: `(blockIdx * outputDim + o)`). */ + fun blockMajor(blocks: Array>): ByteArray { + val rows = blocks.size; val perRow = blocks[0].size + val out = ArrayList() + for (bI in 0 until perRow) for (o in 0 until rows) for (x in blocks[o][bI]) out.add(x) + return out.toByteArray() + } + + fun floats(n: Int, seed: Long, scale: Float = 1f): FloatArray { + val rng = Rng(seed) + return FloatArray(n) { rng.nextSigned() * scale } + } + + // --- digests --- + + private const val FNV_OFFSET = -3750763034362895579L // 0xcbf29ce484222325 + private const val FNV_PRIME = 1099511628211L + + private fun fnvByte(h: Long, b: Int): Long = (h xor (b and 0xFF).toLong()) * FNV_PRIME + + fun digest(values: FloatArray): String { + var h = FNV_OFFSET + for (v in values) { + val bits = v.toRawBits() + h = fnvByte(h, bits); h = fnvByte(h, bits ushr 8); h = fnvByte(h, bits ushr 16); h = fnvByte(h, bits ushr 24) + } + val head = (0 until minOf(4, values.size)).joinToString(",") { hex32(values[it].toRawBits()) } + return "n=${values.size} fnv=${hex64(h)} head=$head" + } + + fun digest(bytes: ByteArray): String { + var h = FNV_OFFSET + for (b in bytes) h = fnvByte(h, b.toInt()) + val head = (0 until minOf(8, bytes.size)).joinToString("") { hex8(bytes[it].toInt()) } + return "n=${bytes.size} fnv=${hex64(h)} head=$head" + } + + private fun hex8(v: Int): String = (v and 0xFF).toString(16).padStart(2, '0') + private fun hex32(v: Int): String = v.toUInt().toString(16).padStart(8, '0') + private fun hex64(v: Long): String = v.toULong().toString(16).padStart(16, '0') + + /** Assert [actual] equals the recorded golden for [name]; the failure message carries the actual digest. */ + fun check(name: String, actual: String) { + val expected = Goldens.expected[name] + ?: fail("No golden recorded for '$name'. Record it in Goldens.kt:\n \"$name\" to \"$actual\",") + if (expected != actual) { + fail( + "Golden mismatch for '$name' — the packed-encoding output is no longer bit-identical.\n" + + " expected: $expected\n actual: $actual\n" + + "If this change is intended (new kernel semantics), re-baseline Goldens.kt in the same PR with the evidence." + ) + } + } +} diff --git a/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/Goldens.kt b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/Goldens.kt new file mode 100644 index 000000000..ffe45f5b7 --- /dev/null +++ b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/Goldens.kt @@ -0,0 +1,39 @@ +package sk.ainet.exec.golden + +/** + * Recorded golden digests (see [GoldenSupport.digest]). One entry per (test, encoding). Recorded on + * JVM (HotSpot, strict binary32) and verified bit-identical on Kotlin/Native linuxX64. + * + * Re-baselining is a deliberate act: change a value only in the PR that intentionally changes the + * numeric behaviour of a decoder or scalar kernel, and say so in the PR. + */ +internal object Goldens { + val expected: Map = mapOf( + "decode/Q4_0" to "n=384 fnv=d6bc5d718cd98a58 head=bdb0dc00,3d979800,00000000,3d4a2000", + "decode/Q4_K" to "n=3072 fnv=fa096e860a45191e head=4049e820,408a0210,409c8910,40c19710", + "decode/Q5_0" to "n=384 fnv=6a72d36758b74185 head=3dc3e000,bdc3e000,bef4d800,be74d800", + "decode/Q5_1" to "n=384 fnv=b33abdb77cf4b87f head=be80e800,bd89d000,3bd50000,3ead5800", + "decode/Q5_K" to "n=3072 fnv=3381529c397a361d head=4125da40,416af240,41980520,41980520", + "decode/Q6_K" to "n=3072 fnv=3c3d7fa00c48faa9 head=c2606550,40ef5b00,41fe50b0,42606550", + "decode/Q8_0" to "n=384 fnv=334c03e32df05c28 head=3f6a1700,bf6ede00,3ee55000,bf786c00", + "decode/TERNARY_tq2_0" to "n=256 fnv=48409a8f0eeaba6e head=bd000000,bd000000,bd000000,bd000000", + "decode/TERNARY_values" to "n=384 fnv=e455e2a9df6acf48 head=00000000,bf500000,bf500000,3f500000", + "scalar-matmul/Q4_0" to "n=12 fnv=0aaee3d77e3619f6 head=3f15be82,3f0f4b4e,3e49fb8e,be220b31", + "scalar-matmul/Q4_K" to "n=12 fnv=1c27f69421ebce94 head=c29e9232,c3116957,c3a2cb4f,c2da54e5", + "scalar-matmul/Q5_0" to "n=12 fnv=9cf4c45d4329bee6 head=3ffafe21,4073853c,c0a97fbe,3e9fe3a0", + "scalar-matmul/Q5_1" to "n=12 fnv=3a57aa4feea433fd head=3fe5eab0,bfe35b0c,40821cc5,40ca2f5a", + "scalar-matmul/Q5_K" to "n=12 fnv=69764ad908eba4c2 head=4317c800,c3350201,4380baa3,4286212d", + "scalar-matmul/Q6_K" to "n=12 fnv=b534bb781b931bb2 head=c41704aa,425d6ccb,4423cdf0,43bffcfd", + "scalar-matmul/Q8_0" to "n=12 fnv=a1ba10e77b91e0ab head=bf462edd,3f92c634,c151b4cb,c0e14735", + "turboquant/polar3/codes" to "n=48 fnv=46f69505e290de9f head=5db9a5a5d84adcb4", + "turboquant/polar4-qjl1/codes" to "n=64 fnv=4e1c32b146ebeda5 head=6a7b76918a83ba43", + "turboquant/polar4/codes" to "n=64 fnv=4e1c32b146ebeda5 head=6a7b76918a83ba43", + "packed/TERNARY_values" to "n=96 fnv=f659cade8a7a5701 head=8192058611602495", + "turboquant/polar3/scales" to "n=4 fnv=91fae3204b7f58d3 head=3fa460a9,3f9ffba5,400b2d5d,3fdc5844", + "turboquant/polar4-qjl1/scales" to "n=4 fnv=296a63c84523f465 head=3f0ce523,3f0920d7,3f6e96e8,3f3cddf1", + "turboquant/polar4/scales" to "n=4 fnv=296a63c84523f465 head=3f0ce523,3f0920d7,3f6e96e8,3f3cddf1", + "turboquant/polar3/decoded" to "n=128 fnv=92a4aba1adab2736 head=3ef321a3,be3f44b9,bf927550,4054d201", + "turboquant/polar4-qjl1/decoded" to "n=128 fnv=c709e3b423fd49ff head=3ef331d0,beddd096,bf501ad5,404d9967", + "turboquant/polar4/decoded" to "n=128 fnv=aad39f2f7c413875 head=3ed15f9f,bebbfe6a,bf71ed00,403cb050", + ) +} diff --git a/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/PackedDecodeGoldenTest.kt b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/PackedDecodeGoldenTest.kt new file mode 100644 index 000000000..2a03c7625 --- /dev/null +++ b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/PackedDecodeGoldenTest.kt @@ -0,0 +1,78 @@ +package sk.ainet.exec.golden + +import sk.ainet.exec.golden.GoldenSupport.Packed +import sk.ainet.lang.tensor.Shape +import sk.ainet.lang.tensor.data.Q4_0BlockTensorData +import sk.ainet.lang.tensor.data.Q4_KBlockTensorData +import sk.ainet.lang.tensor.data.Q5_0BlockTensorData +import sk.ainet.lang.tensor.data.Q5_1BlockTensorData +import sk.ainet.lang.tensor.data.Q5_KBlockTensorData +import sk.ainet.lang.tensor.data.Q6_KBlockTensorData +import sk.ainet.lang.tensor.data.Q8_0BlockTensorData +import sk.ainet.lang.tensor.data.Ternary2BitTensorData +import sk.ainet.lang.tensor.storage.PackedBlockStorage +import kotlin.test.Test + +/** + * SKEEP-003 golden gate, decode half: the block decoders of every packed `TensorData` must produce + * bit-identical floats for the same bytes. Guards the TensorData → TensorView façade migration + * (M1) and every later refactor of the packed storage types. + */ +class PackedDecodeGoldenTest { + + private companion object { + const val ROWS = 4 + const val BLOCKS_PER_ROW = 3 + const val SEED = 0x5EED_0001L + } + + private fun decodeAll(storage: PackedBlockStorage, elementCount: Int, blockSize: Int): FloatArray { + val out = FloatArray(elementCount) + val blocks = (elementCount + blockSize - 1) / blockSize + val tmp = FloatArray(blockSize) + for (b in 0 until blocks) { + storage.dequantizeBlock(b, tmp, 0) + val n = minOf(blockSize, elementCount - b * blockSize) + tmp.copyInto(out, b * blockSize, 0, n) + } + return out + } + + private fun golden(p: Packed, build: (Shape, ByteArray) -> PackedBlockStorage) { + val blocks = GoldenSupport.weightBlocks(p, ROWS, BLOCKS_PER_ROW, SEED) + val shape = Shape(ROWS, BLOCKS_PER_ROW * p.blockSize) + val storage = build(shape, GoldenSupport.rowMajor(blocks)) + val decoded = decodeAll(storage, shape.volume, p.blockSize) + GoldenSupport.check("decode/${p.name}", GoldenSupport.digest(decoded)) + } + + @Test fun q4_0() = golden(Packed.Q4_0) { s, b -> Q4_0BlockTensorData(s, b) } + @Test fun q5_0() = golden(Packed.Q5_0) { s, b -> Q5_0BlockTensorData(s, b) } + @Test fun q5_1() = golden(Packed.Q5_1) { s, b -> Q5_1BlockTensorData(s, b) } + @Test fun q8_0() = golden(Packed.Q8_0) { s, b -> Q8_0BlockTensorData(s, b) } + @Test fun q4_K() = golden(Packed.Q4_K) { s, b -> Q4_KBlockTensorData(s, b) } + @Test fun q5_K() = golden(Packed.Q5_K) { s, b -> Q5_KBlockTensorData(s, b) } + @Test fun q6_K() = golden(Packed.Q6_K) { s, b -> Q6_KBlockTensorData(s, b) } + + @Test + fun ternaryFromValues() { + val rng = GoldenSupport.Rng(SEED + 7) + val n = 4 * 96 + val values = ByteArray(n) { (rng.nextByte().toInt() % 3).toByte() } // -2..2 → clamp to {-1,0,1} + for (i in values.indices) values[i] = values[i].toInt().coerceIn(-1, 1).toByte() + val t = Ternary2BitTensorData.fromTernaryValues(Shape(4, 96), values, scale = 0.8125f) + val decoded = decodeAll(t, n, t.blockSize) + GoldenSupport.check("decode/TERNARY_values", GoldenSupport.digest(decoded)) + GoldenSupport.check("packed/TERNARY_values", GoldenSupport.digest(t.packedData)) + } + + @Test + fun ternaryFromTQ2_0Block() { + val rng = GoldenSupport.Rng(SEED + 11) + val block = ByteArray(66) { rng.nextByte() } + GoldenSupport.le16(block, 64, GoldenSupport.half(0.03125f)) + val t = Ternary2BitTensorData.fromTQ2_0Block(block, Shape(256)) + val decoded = decodeAll(t, 256, t.blockSize) + GoldenSupport.check("decode/TERNARY_tq2_0", GoldenSupport.digest(decoded)) + } +} diff --git a/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/ScalarKernelGoldenTest.kt b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/ScalarKernelGoldenTest.kt new file mode 100644 index 000000000..7adbaa7f3 --- /dev/null +++ b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/ScalarKernelGoldenTest.kt @@ -0,0 +1,43 @@ +package sk.ainet.exec.golden + +import sk.ainet.exec.golden.GoldenSupport.Packed +import sk.ainet.exec.kernel.ScalarKernelProvider +import kotlin.test.Test +import kotlin.test.assertNotNull + +/** + * SKEEP-003 golden gate, kernel half: the pure-Kotlin scalar packed matmul kernels (the reference + * tier every SIMD/native kernel is validated against) must stay bit-identical for the same bytes + * and activations. Guards the KernelKey / registry migration (M1) — a re-routed dispatch that + * still lands on these kernels produces these exact outputs. + */ +class ScalarKernelGoldenTest { + + private companion object { + const val OUT = 4 + const val BLOCKS = 3 + const val BATCH = 3 + const val SEED = 0x5EED_0002L + } + + private fun golden( + p: Packed, + run: (input: FloatArray, inOff: Int, weight: ByteArray, inputDim: Int, outputDim: Int, out: FloatArray, outOff: Int) -> Unit, + ) { + val inputDim = BLOCKS * p.blockSize + val blocks = GoldenSupport.weightBlocks(p, OUT, BLOCKS, SEED) + val weight = GoldenSupport.blockMajor(blocks) + val input = GoldenSupport.floats(BATCH * inputDim, SEED + 100) + val out = FloatArray(BATCH * OUT) + for (r in 0 until BATCH) run(input, r * inputDim, weight, inputDim, OUT, out, r * OUT) + GoldenSupport.check("scalar-matmul/${p.name}", GoldenSupport.digest(out)) + } + + @Test fun q4_0() { val k = assertNotNull(ScalarKernelProvider.matmulQ4_0()); golden(Packed.Q4_0) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } + @Test fun q5_0() { val k = assertNotNull(ScalarKernelProvider.matmulQ5_0()); golden(Packed.Q5_0) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } + @Test fun q5_1() { val k = assertNotNull(ScalarKernelProvider.matmulQ5_1()); golden(Packed.Q5_1) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } + @Test fun q8_0() { val k = assertNotNull(ScalarKernelProvider.matmulQ8_0()); golden(Packed.Q8_0) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } + @Test fun q4_K() { val k = assertNotNull(ScalarKernelProvider.matmulQ4K()); golden(Packed.Q4_K) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } + @Test fun q5_K() { val k = assertNotNull(ScalarKernelProvider.matmulQ5K()); golden(Packed.Q5_K) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } + @Test fun q6_K() { val k = assertNotNull(ScalarKernelProvider.matmulQ6K()); golden(Packed.Q6_K) { i, io, w, n, m, o, oo -> k.matmul(i, io, w, 0, n, m, o, oo) } } +} diff --git a/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/TurboQuantGoldenTest.kt b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/TurboQuantGoldenTest.kt new file mode 100644 index 000000000..82be59f14 --- /dev/null +++ b/skainet-backends/skainet-backend-cpu/src/goldenTest/kotlin/sk/ainet/exec/golden/TurboQuantGoldenTest.kt @@ -0,0 +1,24 @@ +package sk.ainet.exec.golden + +import sk.ainet.lang.tensor.ops.turboquant.TurboQuantCodec +import sk.ainet.lang.tensor.ops.turboquant.TurboQuantConfig +import kotlin.test.Test + +/** + * SKEEP-003 golden gate, TurboQuant: encoded bytes, scales and the decoded vector must stay + * bit-identical for the same input and seed (the KV-cache compression path). + */ +class TurboQuantGoldenTest { + + private fun golden(tag: String, config: TurboQuantConfig) { + val input = GoldenSupport.floats(128, 0x5EED_0003L, scale = 3f) + val block = TurboQuantCodec.encode(input, config) + GoldenSupport.check("turboquant/$tag/codes", GoldenSupport.digest(block.packedCodes)) + GoldenSupport.check("turboquant/$tag/scales", GoldenSupport.digest(block.scales)) + GoldenSupport.check("turboquant/$tag/decoded", GoldenSupport.digest(TurboQuantCodec.decode(block))) + } + + @Test fun polar4() = golden("polar4", TurboQuantConfig.polarOnly(bits = 4, seed = 7)) + @Test fun polar3() = golden("polar3", TurboQuantConfig.polarOnly(bits = 3, seed = 7)) + @Test fun polar4Qjl1() = golden("polar4-qjl1", TurboQuantConfig.polarPlusQjl(bits = 4, residualBits = 1, seed = 7)) +}