diff --git a/skainet-lang/skainet-lang-core/api/jvm/skainet-lang-core.api b/skainet-lang/skainet-lang-core/api/jvm/skainet-lang-core.api index 3a5f25105..588370a16 100644 --- a/skainet-lang/skainet-lang-core/api/jvm/skainet-lang-core.api +++ b/skainet-lang/skainet-lang-core/api/jvm/skainet-lang-core.api @@ -4188,6 +4188,7 @@ public final class sk/ainet/lang/tensor/data/Q4_0BlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getView ()Lsk/ainet/lang/memory/TensorView; @@ -4248,6 +4249,7 @@ public final class sk/ainet/lang/tensor/data/Q4_KBlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getSubBlockMin (II)F @@ -4308,6 +4310,7 @@ public final class sk/ainet/lang/tensor/data/Q5_0BlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getView ()Lsk/ainet/lang/memory/TensorView; @@ -4353,6 +4356,7 @@ public final class sk/ainet/lang/tensor/data/Q5_1BlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getView ()Lsk/ainet/lang/memory/TensorView; @@ -4401,6 +4405,7 @@ public final class sk/ainet/lang/tensor/data/Q5_KBlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getSubBlockMin (II)F @@ -4467,6 +4472,7 @@ public final class sk/ainet/lang/tensor/data/Q6_KBlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getSubBlockScale (II)I @@ -4556,6 +4562,7 @@ public final class sk/ainet/lang/tensor/data/Q8_0BlockTensorData : sk/ainet/lang public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun getView ()Lsk/ainet/lang/memory/TensorView; @@ -4656,6 +4663,7 @@ public final class sk/ainet/lang/tensor/data/Ternary2BitTensorData : sk/ainet/la public fun getElementCount ()J public fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public fun getScale ()F public fun getShape ()Lsk/ainet/lang/tensor/Shape; @@ -6387,6 +6395,7 @@ public abstract interface class sk/ainet/lang/tensor/storage/PackedBlockStorage public fun getElementCount ()J public abstract fun getEncoding ()Lsk/ainet/lang/tensor/storage/TensorEncoding; public abstract fun getPackedData ()[B + public fun getPackedView ()Lsk/ainet/lang/memory/TensorView; public fun getPhysicalBytes ()J public abstract fun getShape ()Lsk/ainet/lang/tensor/Shape; public fun toFloatArray ()[F @@ -6399,6 +6408,7 @@ public abstract interface class sk/ainet/lang/tensor/storage/PackedBlockStorage public final class sk/ainet/lang/tensor/storage/PackedBlockStorage$DefaultImpls { public static synthetic fun dequantizeBlock$default (Lsk/ainet/lang/tensor/storage/PackedBlockStorage;I[FIILjava/lang/Object;)V public static fun getElementCount (Lsk/ainet/lang/tensor/storage/PackedBlockStorage;)J + public static fun getPackedView (Lsk/ainet/lang/tensor/storage/PackedBlockStorage;)Lsk/ainet/lang/memory/TensorView; public static fun getPhysicalBytes (Lsk/ainet/lang/tensor/storage/PackedBlockStorage;)J public static fun toFloatArray (Lsk/ainet/lang/tensor/storage/PackedBlockStorage;)[F public static fun toTensorStorage (Lsk/ainet/lang/tensor/storage/PackedBlockStorage;Lsk/ainet/lang/tensor/storage/LogicalDType;Lsk/ainet/lang/tensor/storage/Placement;)Lsk/ainet/lang/tensor/storage/TensorStorage; diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/Layout.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/Layout.kt index fc07d4a9c..5909ad652 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/Layout.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/Layout.kt @@ -142,11 +142,20 @@ public class Layout( public fun blocked(shape: Shape, blockSize: Int, bytesPerBlock: Int, offsetBlocks: Long = 0L): Layout { require(blockSize > 0 && bytesPerBlock > 0) { "block geometry must be positive" } require(shape.rank >= 1) { "blocked layout needs rank >= 1" } - require(shape[shape.rank - 1] % blockSize == 0) { "last extent ${shape[shape.rank - 1]} is not a multiple of the block size $blockSize" } - val dims = shape.dimensions.copyOf() - dims[dims.size - 1] = dims[dims.size - 1] / blockSize - val blockShape = Shape(dims) - return Layout(blockShape, rowMajorStrides(blockShape), offsetBlocks, bytesPerBlock, blocked = true) + val last = shape[shape.rank - 1] + if (last % blockSize == 0) { + val dims = shape.dimensions.copyOf() + dims[dims.size - 1] = last / blockSize + val blockShape = Shape(dims) + return Layout(blockShape, rowMajorStrides(blockShape), offsetBlocks, bytesPerBlock, blocked = true) + } + // A block that spans rows (e.g. ternary, where the whole tensor is one block): address the + // flattened element sequence instead. Such a view decodes but cannot be sliced per axis. + require(shape.volume % blockSize == 0) { + "neither the last extent ($last) nor the volume (${shape.volume}) is a multiple of the block size $blockSize" + } + val flat = Shape(shape.volume / blockSize) + return Layout(flat, rowMajorStrides(flat), offsetBlocks, bytesPerBlock, blocked = true) } } } diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/TensorView.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/TensorView.kt index da1909b18..9b80e9ef0 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/TensorView.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/memory/TensorView.kt @@ -47,6 +47,7 @@ public class TensorView( */ public fun narrow(axis: Int, from: Int, size: Int): TensorView { require(axis in 0 until shape.rank) { "axis $axis out of range for rank ${shape.rank}" } + check(!(layout.blocked && layout.shape.rank != shape.rank)) { "this view's blocks span rows; slice it after materialize()" } require(from >= 0 && size >= 0 && from + size <= shape[axis]) { "narrow($axis, $from, $size) outside extent ${shape[axis]}" } val onBlockAxis = layout.blocked && axis == shape.rank - 1 val unit = if (onBlockAxis) blockSize() else 1 @@ -122,6 +123,16 @@ public class TensorView( */ private fun flatLogicalIndex(indices: IntArray): Long { val bs = blockSize() + // A block spanning rows (layout flattened by Layout.blocked): plain row-major element index. + if (layout.blocked && layout.shape.rank != shape.rank) { + var flat = 0L + for (d in indices.indices) { + val i = indices[d] + require(i in 0 until shape[d]) { "index $i out of range for axis $d (extent ${shape[d]})" } + flat = flat * shape[d] + i + } + return layout.offsetElements * bs + flat + } val last = indices[indices.size - 1] val blockIdxWithinRow = last / bs val within = last % bs diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_0TensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_0TensorData.kt index c19237213..fdeca6345 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_0TensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_0TensorData.kt @@ -67,6 +67,10 @@ public class Q4_0BlockTensorData( private val data: ByteArray ) : Q4_0TensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_KTensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_KTensorData.kt index ebf27ba5a..8a0655234 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_KTensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q4_KTensorData.kt @@ -94,6 +94,10 @@ public class Q4_KBlockTensorData( private val data: ByteArray ) : Q4_KTensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_0TensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_0TensorData.kt index 61eba8d70..06913fc31 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_0TensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_0TensorData.kt @@ -37,6 +37,10 @@ public class Q5_0BlockTensorData( private val data: ByteArray, ) : Q5_0TensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_1TensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_1TensorData.kt index 52550ca33..bda194f34 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_1TensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_1TensorData.kt @@ -42,6 +42,10 @@ public class Q5_1BlockTensorData( private val data: ByteArray, ) : Q5_1TensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_KTensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_KTensorData.kt index 9aba08bd0..c94bbd927 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_KTensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q5_KTensorData.kt @@ -100,6 +100,10 @@ public class Q5_KBlockTensorData( private val data: ByteArray ) : Q5_KTensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q6_KTensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q6_KTensorData.kt index 73e03ed6f..90f8af1db 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q6_KTensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q6_KTensorData.kt @@ -85,6 +85,10 @@ public class Q6_KBlockTensorData( private val data: ByteArray ) : Q6_KTensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q8_0TensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q8_0TensorData.kt index 673d87199..5ea59a5dd 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q8_0TensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/Q8_0TensorData.kt @@ -54,6 +54,10 @@ public class Q8_0BlockTensorData( private val data: ByteArray ) : Q8_0TensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/TernaryTensorData.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/TernaryTensorData.kt index 63a5106c5..126952435 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/TernaryTensorData.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/data/TernaryTensorData.kt @@ -49,6 +49,10 @@ public class Ternary2BitTensorData( override val scale: Float = 1.0f ) : TernaryTensorData, PackedBlockStorage { + /** The façade over the packed bytes (SKEEP-003 §4.1): see [PackedBlockStorage.packedView]. */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + override val view: sk.ainet.lang.memory.TensorView get() = packedView + override val shape: Shape = Shape(initialShape.dimensions.copyOf()) private val strides: IntArray = shape.computeStrides() override val packedData: ByteArray get() = data diff --git a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/storage/PackedBlockStorage.kt b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/storage/PackedBlockStorage.kt index bcefaa6ab..7501a8765 100644 --- a/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/storage/PackedBlockStorage.kt +++ b/skainet-lang/skainet-lang-core/src/commonMain/kotlin/sk/ainet/lang/tensor/storage/PackedBlockStorage.kt @@ -51,6 +51,25 @@ public interface PackedBlockStorage { * Dequantize the entire tensor to a FloatArray. * Default implementation calls [dequantizeBlock] for each block. */ + /** + * This packed data as a [sk.ainet.lang.memory.TensorView] — `Format(FP32, encoding)` over a + * blocked [sk.ainet.lang.memory.Layout] whose storage **borrows** [packedData] (SKEEP-003 §4.1 + * façade, rule 5). Nothing is copied: slicing or transposing the view addresses whole blocks + * and the bytes stay exactly as the loader produced them, which is what keeps every packed + * kernel bit-identical. + * + * `view.get(...)` decodes through [dequantizeBlock] (rule 4) — it never returns a raw byte, + * unlike this data's own `get`, which stays as it is for source compatibility. + */ + @sk.ainet.lang.memory.ExperimentalMemoryApi + public val packedView: sk.ainet.lang.memory.TensorView + get() = sk.ainet.lang.memory.TensorView.packed( + storage = sk.ainet.lang.memory.Storage.Heap.wrap(packedData, mutable = false), + shape = shape, + encoding = encoding, + decoder = sk.ainet.lang.memory.PackedBlockDecoder(this), + ) + public fun toFloatArray(): FloatArray { val result = FloatArray(shape.volume) var offset = 0 diff --git a/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/lang/memory/PackedTensorDataViewTest.kt b/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/lang/memory/PackedTensorDataViewTest.kt new file mode 100644 index 000000000..5b08bef3f --- /dev/null +++ b/skainet-lang/skainet-lang-core/src/commonTest/kotlin/sk/ainet/lang/memory/PackedTensorDataViewTest.kt @@ -0,0 +1,116 @@ +package sk.ainet.lang.memory + +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 sk.ainet.lang.tensor.storage.TensorEncoding +import sk.ainet.lang.types.FP32 +import kotlin.test.Test +import kotlin.test.assertContentEquals +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertSame +import kotlin.test.assertTrue + +/** + * SKEEP-003 §4.1 façade for the packed encodings, with the constraint that matters most: the view + * decodes **bit-identically** to the existing block decoders and borrows the very bytes the loader + * produced (rule 5 — nothing is copied, nothing is re-ordered). + */ +@OptIn(ExperimentalMemoryApi::class) +class PackedTensorDataViewTest { + + /** Deterministic bytes with sane FP16 scales, as in the golden parity fixtures. */ + private class Rng(seed: Long) { + private var s = seed + fun next(): Long { var x = s; x = x xor (x shl 13); x = x xor (x ushr 7); x = x xor (x shl 17); s = x; return x } + fun byte(): Byte = (next() ushr 33).toByte() + fun unit(): Float = ((next() ushr 40).toInt() and 0xFFFF) / 65536f + } + + private fun half(v: Float): Int { + val b = v.toRawBits(); val sign = (b ushr 16) and 0x8000 + val e = ((b ushr 23) and 0xFF) - 127 + 15; val m = b and 0x7FFFFF + if (e <= 0) return sign; if (e >= 31) return sign or 0x7C00 + return sign or (e shl 10) or (m 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() } + + private fun blocks(count: Int, bytesPerBlock: Int, seed: Long, scaleOffsets: List): ByteArray { + val rng = Rng(seed) + val b = ByteArray(count * bytesPerBlock) { rng.byte() } + for (blk in 0 until count) for (off in scaleOffsets) le16(b, blk * bytesPerBlock + off, half(rng.unit() * 0.05f + 0.005f)) + return b + } + + private fun check(name: String, data: PackedBlockStorage, encoding: TensorEncoding, packedBytes: ByteArray) { + val v = data.packedView + // format: logically FP32, physically the block encoding (rule 3) + assertEquals(Format(FP32, encoding), v.format, "$name format") + assertFalse(v.format.isDense) + // zero-copy: the storage borrows the loader's bytes + assertSame(packedBytes, (v.storage as Storage.Heap).bytes, "$name borrows its bytes") + assertFalse(v.storage.isMutable, "$name view is read-only") + // bit-identical decode: block-wise (PackedBlockStorage) vs element-wise (view.get) + val expected = data.toFloatArray() + val actual = v.toFloatArray() + assertEquals(expected.size, actual.size, "$name element count") + for (i in expected.indices) { + assertEquals(expected[i].toRawBits(), actual[i].toRawBits(), "$name element $i: ${expected[i]} vs ${actual[i]}") + } + // get() decodes, never a raw byte + assertEquals(expected[0].toRawBits(), v.get(0, 0).toRawBits(), "$name get(0,0)") + assertFailsWith { v.set(0, 0, value = 1f) } + } + + @Test fun q4_0() { val b = blocks(4, 18, 1, listOf(0)); check("Q4_0", Q4_0BlockTensorData(Shape(2, 64), b), TensorEncoding.Q4_0, b) } + @Test fun q5_0() { val b = blocks(4, 22, 2, listOf(0)); check("Q5_0", Q5_0BlockTensorData(Shape(2, 64), b), TensorEncoding.Q5_0, b) } + @Test fun q5_1() { val b = blocks(4, 24, 3, listOf(0, 2)); check("Q5_1", Q5_1BlockTensorData(Shape(2, 64), b), TensorEncoding.Q5_1, b) } + @Test fun q8_0() { val b = blocks(4, 34, 4, listOf(0)); check("Q8_0", Q8_0BlockTensorData(Shape(2, 64), b), TensorEncoding.Q8_0, b) } + @Test fun q4_K() { val b = blocks(2, 144, 5, listOf(0, 2)); check("Q4_K", Q4_KBlockTensorData(Shape(2, 256), b), TensorEncoding.Q4_K, b) } + @Test fun q5_K() { val b = blocks(2, 176, 6, listOf(0, 2)); check("Q5_K", Q5_KBlockTensorData(Shape(2, 256), b), TensorEncoding.Q5_K, b) } + @Test fun q6_K() { val b = blocks(2, 210, 7, listOf(208)); check("Q6_K", Q6_KBlockTensorData(Shape(2, 256), b), TensorEncoding.Q6_K, b) } + + @Test + fun ternary() { + val values = ByteArray(4 * 32) { ((it % 3) - 1).toByte() } + val t = Ternary2BitTensorData.fromTernaryValues(Shape(4, 32), values, scale = 0.8125f) + check("Ternary", t, TensorEncoding.TernaryPacked, t.packedData) + } + + @Test + fun packedViewsSliceWholeBlocksOverTheSameBytes() { + val b = blocks(4, 144, 11, listOf(0, 2)) // 4 Q4_K blocks = [2, 512] + val data = Q4_KBlockTensorData(Shape(2, 512), b) + val v = data.packedView + assertTrue(v.layout.blocked); assertEquals(Shape(2, 2), v.layout.shape) + val row1 = v.narrow(0, 1, 1) + assertSame(v.storage, row1.storage) // no copy + val all = data.toFloatArray() + val expectedRow = all.copyOfRange(512, 1024) + val actualRow = row1.toFloatArray() + for (i in expectedRow.indices) assertEquals(expectedRow[i].toRawBits(), actualRow[i].toRawBits(), "row element $i") + assertFailsWith { v.narrow(1, 0, 100) } // partial block + } + + @Test + fun materializingAPackedViewDecodesIntoAScope() { + val b = blocks(2, 34, 13, listOf(0)) + val data = Q8_0BlockTensorData(Shape(2, 32), b) + val scope = ForwardScope(128) + val dense = data.packedView.materialize(scope = scope) + assertTrue(dense.format.isDense); assertEquals(ScopeKind.FORWARD, dense.storage.scope) + val expected = data.toFloatArray() + for (i in expected.indices) assertEquals(expected[i].toRawBits(), dense.toFloatArray()[i].toRawBits()) + scope.close() + } +}