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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@

### Fixed

- **`TensorData.copyToFloatArray()` default implementation works for rank >= 2.** It used to
iterate a single flat index into the vararg `get`, tripping every implementation's
one-index-per-dimension arity check — a latent trap for any implementation that didn't
override it. The default now unravels flat positions into per-dimension indices (row-major);
a contract test exercises the default at ranks 1–3. (#930)
- **Streaming GGUF loads fail fast on unsupported tensor types instead of silently skipping them.**
`StreamingGgufParametersLoader` used to emit a `SKIP` progress string for any tensor type outside
its `when` and deliver a model with silently missing weights — the failure then surfaced far away
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,23 @@ public interface TensorData<T : DType, V> : ItemsAccessor<V> {
* The default implementation iterates over all elements, which may be slow for backends
* where individual element access is expensive (e.g., GPU tensors).
*
* The default unravels each flat position into per-dimension indices, because [get]
* requires exactly one index per dimension — a single flat index would trip every
* implementation's arity check for rank >= 2 tensors.
*
* @return a new FloatArray containing all tensor values in row-major order
*/
public fun copyToFloatArray(): FloatArray {
val dims = shape.dimensions
val volume = shape.volume
return FloatArray(volume) { idx ->
(get(idx) as Number).toFloat()
val indices = IntArray(dims.size)
return FloatArray(volume) { flat ->
var remaining = flat
for (d in dims.indices.reversed()) {
indices[d] = remaining % dims[d]
remaining /= dims[d]
}
(get(*indices) as Number).toFloat()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package sk.ainet.lang.tensor.data

import sk.ainet.lang.tensor.Shape
import sk.ainet.lang.types.FP32
import kotlin.test.Test
import kotlin.test.assertContentEquals

/**
* Contract test for the DEFAULT [TensorData.copyToFloatArray] implementation.
*
* The implementation used to iterate a single flat index into the vararg
* [TensorData.get], which trips the one-index-per-dimension arity check of
* every implementation for rank >= 2 tensors. The fixture below deliberately
* does NOT override copyToFloatArray, so it exercises the interface default.
*/
class TensorDataDefaultCopyTest {

/** Minimal implementation that inherits the default copyToFloatArray. */
private class MinimalTensorData(
override val shape: Shape,
private val values: FloatArray,
) : TensorData<FP32, Float> {

private fun flatten(indices: IntArray): Int {
require(indices.size == shape.dimensions.size) {
"Expected ${shape.dimensions.size} indices, got ${indices.size}"
}
var flat = 0
for (d in indices.indices) {
require(indices[d] in 0 until shape.dimensions[d]) { "Index out of bounds" }
flat = flat * shape.dimensions[d] + indices[d]
}
return flat
}

override fun get(vararg indices: Int): Float = values[flatten(indices)]

override fun set(vararg indices: Int, value: Float) {
values[flatten(indices)] = value
}
}

@Test
fun default_copy_works_for_rank_1() {
val data = MinimalTensorData(Shape(4), floatArrayOf(1f, 2f, 3f, 4f))
assertContentEquals(floatArrayOf(1f, 2f, 3f, 4f), data.copyToFloatArray())
}

@Test
fun default_copy_works_for_rank_2_row_major() {
val data = MinimalTensorData(Shape(2, 3), floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f))
assertContentEquals(floatArrayOf(1f, 2f, 3f, 4f, 5f, 6f), data.copyToFloatArray())
}

@Test
fun default_copy_works_for_rank_3_row_major() {
val values = FloatArray(24) { it.toFloat() }
val data = MinimalTensorData(Shape(2, 3, 4), values)
assertContentEquals(values, data.copyToFloatArray())
}
}
Loading