Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions skainet-lang/skainet-lang-core/api/jvm/skainet-lang-core.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Int>): 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<IllegalStateException> { 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<IllegalArgumentException> { 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()
}
}
Loading