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
44 changes: 44 additions & 0 deletions skainet-lang/skainet-lang-core/api/jvm/skainet-lang-core.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,11 @@ public class TensorView(
public fun get(vararg indices: Int): Float {
storage.checkAlive()
require(indices.size == shape.rank) { "expected ${shape.rank} indices, got ${indices.size}" }
if (format.isDense) return readDense(flatDenseIndex(indices))
val d = decoder ?: throw IllegalStateException("no decoder for ${format.encoding.name}")
val flat = flatLogicalIndex(indices)
return d.decodeElement(storage, layout, flat)
// A decoder wins over the plain path: narrow floats are Dense(2) yet still need decoding.
val d = decoder
if (d != null) return d.decodeElement(storage, layout, flatLogicalIndex(indices))
check(format.isDense) { "no decoder for ${format.encoding.name}" }
return readDense(flatDenseIndex(indices))
}

/** Write [value] at [indices] (dense, mutable views only). */
Expand All @@ -115,7 +116,10 @@ public class TensorView(
return flat
}

/** Logical element index for a packed view: the layout addresses blocks, so the last axis contributes elements. */
/**
* Logical element index for a decoded view: the layout addresses blocks (one element per block
* for narrow floats), so the last axis contributes both a block step and an offset inside it.
*/
private fun flatLogicalIndex(indices: IntArray): Long {
val bs = blockSize()
val last = indices[indices.size - 1]
Expand Down Expand Up @@ -220,3 +224,27 @@ public class PackedBlockDecoder(private val packed: PackedBlockStorage) : BlockD
packed.dequantizeBlock(blockIndex.toInt(), out, outOffset)
}
}

/**
* Decoder for 16-bit narrow floats (FP16 / BF16) held two bytes per element — the "block" is one
* element, so a narrow-float view decodes element by element through its [codec].
*/
@ExperimentalMemoryApi
public class NarrowFloatDecoder(private val codec: sk.ainet.lang.types.NarrowFloatCodec) : BlockDecoder {
override val blockSize: Int get() = 1
override val bytesPerBlock: Int get() = codec.bytesPerElement

override fun decodeBlock(storage: Storage, blockIndex: Long, out: FloatArray, outOffset: Int) {
out[outOffset] = decodeAt(storage, blockIndex)
}

override fun decodeElement(storage: Storage, layout: Layout, flatElementIndex: Long): Float = decodeAt(storage, flatElementIndex)

private fun decodeAt(storage: Storage, elementIndex: Long): Float {
val heap = storage as? Storage.Heap ?: throw UnsupportedOperationException("narrow-float views need heap storage in this milestone")
val bytes = heap.bytes ?: throw UnsupportedOperationException("narrow-float views need byte storage")
val off = heap.arrayOffset + (elementIndex * codec.bytesPerElement).toInt()
val bits = (bytes[off].toInt() and 0xFF) or ((bytes[off + 1].toInt() and 0xFF) shl 8)
return codec.decode(bits)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,10 +98,11 @@ public class LazyMaterializationStrategy<T : DType, V> : MaterializationStrategy
* Lazy tensor data implementation with sparse element caching.
*/
private class LazyMaterializedTensorData<T : DType, V>(
private val view: TensorView<T, V>
// named `sourceView`, not `view`: TensorData.view is the memory-model view (SKEEP-003)
private val sourceView: TensorView<T, V>
) : TensorData<T, V> {

override val shape: Shape = view.viewShape
override val shape: Shape = sourceView.viewShape

// Cache for materialized elements
// Using a map to store only accessed elements
Expand All @@ -113,7 +114,7 @@ public class LazyMaterializationStrategy<T : DType, V> : MaterializationStrategy
// Check if element is already cached
return elementCache[cacheKey] ?: run {
// Element not cached, fetch from view and cache it
val element = view.data.get(*indices)
val element = sourceView.data.get(*indices)
elementCache[cacheKey] = element
element
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,25 @@ public open class NarrowFloatDenseTensorData(
/** Physically two bytes per element whatever the declared dtype witness. */
override val encoding: TensorEncoding get() = TensorEncoding.Dense(NarrowFloatTensorData.BYTES_PER_ELEMENT)

/**
* A view over the *same* packed bytes, decoded by this data's [codec] (SKEEP-003 §4.1 façade).
* The dtype is the codec's (FP16 or BF16) and the encoding `Dense(2)`; `view.get()` returns the
* decoded float, exactly like [get].
*/
@sk.ainet.lang.memory.ExperimentalMemoryApi
override val view: sk.ainet.lang.memory.TensorView
get() = sk.ainet.lang.memory.TensorView(
shape = shape,
format = sk.ainet.lang.memory.Format(codec.dtype, TensorEncoding.Dense(NarrowFloatTensorData.BYTES_PER_ELEMENT)),
layout = sk.ainet.lang.memory.Layout(
shape = shape,
strides = sk.ainet.lang.memory.Layout.rowMajorStrides(shape),
elementBytes = NarrowFloatTensorData.BYTES_PER_ELEMENT,
),
storage = sk.ainet.lang.memory.Storage.Heap.wrap(data, mutable = false),
decoder = sk.ainet.lang.memory.NarrowFloatDecoder(codec),
)

init {
val requiredBytes = shape.volume * NarrowFloatTensorData.BYTES_PER_ELEMENT
require(data.size >= requiredBytes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,20 @@ public interface TensorData<T : DType, V> : ItemsAccessor<V> {
*/
public val encoding: sk.ainet.lang.tensor.storage.TensorEncoding? get() = null

/**
* This data as a [sk.ainet.lang.memory.TensorView] — `Shape + Format + Layout + Storage` — or
* `null` when the implementation cannot expose one (SKEEP-003 §4.1: `TensorData` becomes a
* façade over the view; migrated kernels take the view, everything else keeps using this
* interface unchanged).
*
* The view is over the *same* bytes: for array-backed data the storage borrows the array
* (`Storage.Heap.wrap`), so writes through either side are visible on both and nothing is
* copied. Per-element access stays on this interface's own fast path — the Phase-2 spike
* (#1016) showed a view is for unwrapping once per call, not for per-element reads.
*/
@sk.ainet.lang.memory.ExperimentalMemoryApi
public val view: sk.ainet.lang.memory.TensorView? get() = null

/**
* Copies all tensor data to a FloatArray.
*
Expand Down Expand Up @@ -119,11 +133,29 @@ public interface FloatArrayTensorData<T : DType> : TensorData<T, Float> {
public val buffer: FloatArray

override fun copyToFloatArray(): FloatArray = buffer.copyOf()

/** A dense FP32 view borrowing [buffer] — zero-copy, the same bytes this data reads and writes. */
@sk.ainet.lang.memory.ExperimentalMemoryApi
override val view: sk.ainet.lang.memory.TensorView
get() = sk.ainet.lang.memory.TensorView.dense(
sk.ainet.lang.memory.Storage.Heap.wrap(buffer),
shape,
sk.ainet.lang.types.FP32,
)
}

/**
* Marker interface for tensor data backed by a contiguous `IntArray`.
*/
public interface IntArrayTensorData<T : DType> : TensorData<T, Int> {
public val buffer: IntArray

/** A dense Int32 view borrowing [buffer] — zero-copy, the same bytes this data reads and writes. */
@sk.ainet.lang.memory.ExperimentalMemoryApi
override val view: sk.ainet.lang.memory.TensorView
get() = sk.ainet.lang.memory.TensorView.dense(
sk.ainet.lang.memory.Storage.Heap.wrap(buffer),
shape,
sk.ainet.lang.types.Int32,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package sk.ainet.lang.memory

import sk.ainet.lang.tensor.Shape
import sk.ainet.lang.tensor.data.Bf16DenseTensorData
import sk.ainet.lang.tensor.data.DenseFloatArrayTensorData
import sk.ainet.lang.tensor.data.DenseIntArrayTensorData
import sk.ainet.lang.tensor.data.Fp16DenseTensorData
import sk.ainet.lang.tensor.data.LazyZeroFloatArrayTensorData
import sk.ainet.lang.tensor.data.TensorData
import sk.ainet.lang.types.BF16
import sk.ainet.lang.types.FP16
import sk.ainet.lang.types.FP32
import sk.ainet.lang.types.Int32
import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

/**
* SKEEP-003 §4.1: `TensorData` becomes a façade over `TensorView` — the view is over the *same*
* bytes (borrowed, zero-copy), reads agree with the data's own accessors, and nothing is copied.
*/
@OptIn(ExperimentalMemoryApi::class)
class TensorDataViewTest {

@Test
fun denseFloatDataExposesAZeroCopyView() {
val buf = FloatArray(6) { it.toFloat() }
val data = DenseFloatArrayTensorData<FP32>(Shape(2, 3), buf)
val v = assertNotNull(data.view)
assertEquals(Format.dense(FP32), v.format); assertEquals(Shape(2, 3), v.shape); assertTrue(v.isContiguous)
// same bytes, no copy: the storage borrows the array
assertSame(buf, (v.storage as Storage.Heap).floats)
assertEquals(ScopeKind.AMBIENT, v.storage.scope)
// reads agree
for (i in 0 until 2) for (j in 0 until 3) assertEquals(data.get(i, j), v.get(i, j))
// writes are visible through both
data.set(1, 2, value = 42f); assertEquals(42f, v.get(1, 2))
v.set(0, 0, value = -1f); assertEquals(-1f, data.get(0, 0)); assertEquals(-1f, buf[0])
assertContentEquals(data.copyToFloatArray(), v.toFloatArray())
}

@Test
fun denseIntDataExposesAnInt32View() {
val buf = IntArray(4) { it * 10 }
val data = DenseIntArrayTensorData<Int32>(Shape(4), buf)
val v = assertNotNull(data.view)
assertEquals(Format.dense(Int32), v.format)
assertSame(buf, (v.storage as Storage.Heap).ints)
assertEquals(20f, v.get(2))
}

@Test
fun lazyZeroDataMaterializesThroughTheView() {
val data = LazyZeroFloatArrayTensorData<FP32>(Shape(2, 2))
val v = assertNotNull(data.view)
assertContentEquals(FloatArray(4), v.toFloatArray())
data.set(1, 1, value = 5f)
assertEquals(5f, assertNotNull(data.view).get(1, 1)) // the view is over the materialized buffer
}

@Test
fun narrowFloatDataDecodesThroughTheView() {
// BF16: the high 16 bits of the float
fun bf16(v: Float): Int = (v.toRawBits() ushr 16) and 0xFFFF
val values = floatArrayOf(1f, -2.5f, 0.5f, 100f)
val bytes = ByteArray(values.size * 2)
for ((i, x) in values.withIndex()) { val b = bf16(x); bytes[i * 2] = (b and 0xFF).toByte(); bytes[i * 2 + 1] = ((b ushr 8) and 0xFF).toByte() }
val data = Bf16DenseTensorData(Shape(4), bytes)
val v = assertNotNull(data.view)
assertEquals(BF16, v.format.dtype); assertEquals(2, v.layout.elementBytes)
for (i in values.indices) assertEquals(data.get(i), v.get(i), "element $i")
assertContentEquals(data.copyToFloatArray(), v.toFloatArray())
assertSame(bytes, (v.storage as Storage.Heap).bytes)

val fp16 = Fp16DenseTensorData(Shape(2), ByteArray(4) { (it * 17).toByte() })
val fv = assertNotNull(fp16.view)
assertEquals(FP16, fv.format.dtype)
assertEquals(fp16.get(0), fv.get(0)); assertEquals(fp16.get(1), fv.get(1))
}

@Test
fun dataWithoutAViewReportsNull() {
val anonymous = object : TensorData<FP32, Float> {
override val shape: Shape = Shape(1)
override fun get(vararg indices: Int): Float = 0f
override fun set(vararg indices: Int, value: Float) {}
}
assertNull(anonymous.view)
assertNull(anonymous.encoding)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ public class MemorySegmentTensorData<T : DType> private constructor(
override val segmentByteOffset: Long,
private val ownsArena: Boolean,
) : TensorData<T, Float>, MemorySegmentBackedData {
/**
* A dense view over the *same* off-heap bytes (SKEEP-003 §4.1 façade): the storage borrows this
* data's [segment] — nothing is copied and a migrated kernel unwraps it once with
* `SegmentStorage.segment()`.
*/
@sk.ainet.lang.memory.ExperimentalMemoryApi
override val view: sk.ainet.lang.memory.TensorView
get() = sk.ainet.lang.memory.TensorView.dense(
sk.ainet.lang.memory.SegmentStorage.borrow(
if (segmentByteOffset == 0L) segment else segment.asSlice(segmentByteOffset),
),
shape,
sk.ainet.lang.types.FP32,
)


override val shape: Shape = Shape(initialShape.dimensions.copyOf())
private val strides: IntArray = shape.computeStrides()
Expand Down
Loading