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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import sk.ainet.lang.tensor.data.FloatArrayTensorData
import sk.ainet.lang.tensor.data.TensorDataFactory
import sk.ainet.lang.tensor.ops.UpsampleMode
import sk.ainet.lang.types.FP32
import kotlin.math.ln
import kotlin.math.log10 as kmLog10
import kotlin.math.log2 as kmLog2
import kotlin.math.pow
import kotlin.math.sqrt

@Backend(id = "cpu", displayName = "CPU")
Expand Down Expand Up @@ -2123,6 +2127,112 @@ public open class DefaultCpuOpsBase(protected val dataFactory: TensorDataFactory
return newTensor(outData, tensor.dtype, tensor)
}

/**
* Element-wise power: `c[i] = a[i] ^ b[i]`. Integer-valued exponents
* use repeated multiply for stability; everything else routes through
* `kotlin.math.pow`. Shape contract: shapes must match exactly (no
* broadcasting yet — caller's responsibility).
*/
override fun <T : DType, V> pow(a: Tensor<T, V>, b: Tensor<T, V>): Tensor<T, V> {
require(
a.dtype == sk.ainet.lang.types.FP32::class ||
a.dtype == sk.ainet.lang.types.FP16::class
) { "pow supports only FP16/FP32, got ${a.dtype}" }
require(a.shape == b.shape) { "pow requires matching shapes; got ${a.shape} and ${b.shape}" }
val outData = dataFactory.init<T, V>(a.shape, a.dtype) { idx ->
val av = a.data.get(*idx) as Float
val bv = b.data.get(*idx) as Float
@Suppress("UNCHECKED_CAST")
scalarPow(av, bv) as V
}
return newTensor(outData, a.dtype, a)
}

/**
* Element-wise scalar power: `c[i] = a[i] ^ n`. Small-integer
* exponents (|n| <= 16) use repeated multiply for exactness; all
* other values route through `kotlin.math.pow`.
*/
override fun <T : DType, V> powScalar(a: Tensor<T, V>, n: Number): Tensor<T, V> {
require(
a.dtype == sk.ainet.lang.types.FP32::class ||
a.dtype == sk.ainet.lang.types.FP16::class
) { "powScalar supports only FP16/FP32, got ${a.dtype}" }
val nFloat = n.toFloat()
val nInt = n.toInt()
val isSmallInt = nFloat == nInt.toFloat() && kotlin.math.abs(nInt) <= 16
val outData = dataFactory.init<T, V>(a.shape, a.dtype) { idx ->
val av = a.data.get(*idx) as Float
@Suppress("UNCHECKED_CAST")
(if (isSmallInt) integerPow(av, nInt) else scalarPow(av, nFloat)) as V
}
return newTensor(outData, a.dtype, a)
}

/** Repeated-multiply for small integer exponents. Handles n < 0 via reciprocal. */
private fun integerPow(base: Float, n: Int): Float {
if (n == 0) return 1f
if (n < 0) return 1f / integerPow(base, -n)
var result = 1f
var b = base
var e = n
while (e > 0) {
if (e and 1 == 1) result *= b
b *= b
e = e ushr 1
}
return result
}

private fun scalarPow(base: Float, exp: Float): Float =
base.toDouble().pow(exp.toDouble()).toFloat()

/**
* Element-wise natural log: `c[i] = ln(a[i])`. Negative or zero
* inputs follow `kotlin.math.ln` semantics (negative → NaN, zero
* → -Infinity). Mirror of `stablehlo.log`.
*/
override fun <T : DType, V> log(tensor: Tensor<T, V>): Tensor<T, V> {
require(
tensor.dtype == sk.ainet.lang.types.FP32::class ||
tensor.dtype == sk.ainet.lang.types.FP16::class
) { "log supports only FP16/FP32, got ${tensor.dtype}" }
val outData = dataFactory.init<T, V>(tensor.shape, tensor.dtype) { idx ->
val v = tensor.data.get(*idx) as Float
@Suppress("UNCHECKED_CAST")
ln(v) as V
}
return newTensor(outData, tensor.dtype, tensor)
}

/** Element-wise base-2 log: `c[i] = log2(a[i])`. */
override fun <T : DType, V> log2(tensor: Tensor<T, V>): Tensor<T, V> {
require(
tensor.dtype == sk.ainet.lang.types.FP32::class ||
tensor.dtype == sk.ainet.lang.types.FP16::class
) { "log2 supports only FP16/FP32, got ${tensor.dtype}" }
val outData = dataFactory.init<T, V>(tensor.shape, tensor.dtype) { idx ->
val v = tensor.data.get(*idx) as Float
@Suppress("UNCHECKED_CAST")
kmLog2(v) as V
}
return newTensor(outData, tensor.dtype, tensor)
}

/** Element-wise base-10 log: `c[i] = log10(a[i])`. */
override fun <T : DType, V> log10(tensor: Tensor<T, V>): Tensor<T, V> {
require(
tensor.dtype == sk.ainet.lang.types.FP32::class ||
tensor.dtype == sk.ainet.lang.types.FP16::class
) { "log10 supports only FP16/FP32, got ${tensor.dtype}" }
val outData = dataFactory.init<T, V>(tensor.shape, tensor.dtype) { idx ->
val v = tensor.data.get(*idx) as Float
@Suppress("UNCHECKED_CAST")
kmLog10(v) as V
}
return newTensor(outData, tensor.dtype, tensor)
}

// ---- TinyFoA ops: abs, sign, clamp, lt, ge ----

@TensorOp()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package sk.ainet.exec.tensor.ops

import kotlin.math.abs
import kotlin.math.ln
import kotlin.math.log10 as kmLog10
import kotlin.math.log2 as kmLog2
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import sk.ainet.lang.tensor.Shape
import sk.ainet.lang.tensor.VoidOpsTensor
import sk.ainet.lang.tensor.data.DenseTensorDataFactory
import sk.ainet.lang.tensor.data.FloatArrayTensorData
import sk.ainet.lang.types.FP32
import sk.ainet.lang.types.Int32

/**
* Forward-parity tests for the new `log`, `log2`, `log10` ops (Tier B
* of #617). Verifies against `kotlin.math.ln/log2/log10` per element,
* plus the dtype-restriction guard.
*/
class DefaultCpuOpsLogTest {
private val dataFactory = DenseTensorDataFactory()
private val ops = DefaultCpuOps(dataFactory)

private fun floatTensor(shape: Shape, values: FloatArray) =
VoidOpsTensor(dataFactory.fromFloatArray<FP32, Float>(shape, FP32::class, values), FP32::class)

private fun assertCloseTo(expected: FloatArray, actual: FloatArray, tol: Float = 1e-5f) {
assertEquals(expected.size, actual.size, "length mismatch")
for (i in expected.indices) {
val diff = abs(expected[i] - actual[i])
assertTrue(diff <= tol, "[$i] expected=${expected[i]} actual=${actual[i]} diff=$diff tol=$tol")
}
}

@Test
fun log_matches_kotlin_math_ln() {
val a = floatTensor(Shape(5), floatArrayOf(1f, 2f, kotlin.math.E.toFloat(), 10f, 100f))
val expected = floatArrayOf(0f, ln(2f), 1f, ln(10f), ln(100f))
val out = ops.log(a)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun log2_matches_kotlin_math_log2() {
val a = floatTensor(Shape(5), floatArrayOf(1f, 2f, 4f, 8f, 1024f))
val expected = floatArrayOf(0f, 1f, 2f, 3f, 10f)
val out = ops.log2(a)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun log10_matches_kotlin_math_log10() {
val a = floatTensor(Shape(4), floatArrayOf(1f, 10f, 100f, 1000f))
val expected = floatArrayOf(0f, 1f, 2f, 3f)
val out = ops.log10(a)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun log_of_negative_returns_nan() {
val a = floatTensor(Shape(2), floatArrayOf(-1f, -2f))
val out = ops.log(a)
for (v in (out.data as FloatArrayTensorData<*>).buffer) {
assertTrue(v.isNaN(), "log of negative must be NaN, got $v")
}
}

@Test
fun log_of_zero_returns_negative_infinity() {
val a = floatTensor(Shape(1), floatArrayOf(0f))
val out = ops.log(a)
val result = (out.data as FloatArrayTensorData<*>).buffer[0]
assertEquals(Float.NEGATIVE_INFINITY, result, "log(0) must be -Inf, got $result")
}

@Test
fun log_log2_log10_consistent_with_each_other() {
// log_b(x) = ln(x) / ln(b) — verify the three flavours agree.
val a = floatTensor(Shape(3), floatArrayOf(2f, 10f, 100f))
val logVals = (ops.log(a).data as FloatArrayTensorData<*>).buffer
val log2Vals = (ops.log2(a).data as FloatArrayTensorData<*>).buffer
val log10Vals = (ops.log10(a).data as FloatArrayTensorData<*>).buffer
for (i in 0..2) {
assertEquals(log2Vals[i], logVals[i] / ln(2f), 1e-5f, "log2 consistency at $i")
assertEquals(log10Vals[i], logVals[i] / ln(10f), 1e-5f, "log10 consistency at $i")
}
}

@Test
fun log_rejects_non_float_dtype() {
val intData = dataFactory.fromIntArray<Int32, Int>(Shape(2), Int32::class, intArrayOf(1, 2))
val tInt = VoidOpsTensor(intData, Int32::class)
assertFailsWith<IllegalArgumentException> { ops.log(tInt) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package sk.ainet.exec.tensor.ops

import kotlin.math.abs
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertTrue
import sk.ainet.lang.tensor.Shape
import sk.ainet.lang.tensor.VoidOpsTensor
import sk.ainet.lang.tensor.data.DenseTensorDataFactory
import sk.ainet.lang.tensor.data.FloatArrayTensorData
import sk.ainet.lang.types.FP32

/**
* Forward-parity tests for the new `pow` and `powScalar` ops (Tier A
* of #617). Checks both the binary form (tensor exponent) and the
* scalar form for integer + real exponents.
*/
class DefaultCpuOpsPowTest {
private val dataFactory = DenseTensorDataFactory()
private val ops = DefaultCpuOps(dataFactory)

private fun floatTensor(shape: Shape, values: FloatArray) =
VoidOpsTensor(dataFactory.fromFloatArray<FP32, Float>(shape, FP32::class, values), FP32::class)

private fun assertCloseTo(expected: FloatArray, actual: FloatArray, tol: Float = 1e-4f) {
assertEquals(expected.size, actual.size, "length mismatch")
for (i in expected.indices) {
val diff = abs(expected[i] - actual[i])
assertTrue(diff <= tol, "[$i] expected=${expected[i]} actual=${actual[i]} diff=$diff tol=$tol")
}
}

@Test
fun powScalar_integer_2_matches_x_times_x() {
val a = floatTensor(Shape(5), floatArrayOf(0.5f, 1f, 2f, 3f, -2f))
val expected = floatArrayOf(0.25f, 1f, 4f, 9f, 4f)
val out = ops.powScalar(a, 2)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun powScalar_integer_3_matches_x_cubed() {
val a = floatTensor(Shape(4), floatArrayOf(1f, 2f, 3f, -2f))
val expected = floatArrayOf(1f, 8f, 27f, -8f)
val out = ops.powScalar(a, 3)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun powScalar_negative_integer_minus_1_is_reciprocal() {
val a = floatTensor(Shape(3), floatArrayOf(2f, 4f, 0.5f))
val expected = floatArrayOf(0.5f, 0.25f, 2f)
val out = ops.powScalar(a, -1)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun powScalar_real_half_is_sqrt() {
val a = floatTensor(Shape(4), floatArrayOf(0f, 1f, 4f, 9f))
val expected = floatArrayOf(0f, 1f, 2f, 3f)
val out = ops.powScalar(a, 0.5f)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun powScalar_real_1_5_matches_kotlin_math_pow() {
val a = floatTensor(Shape(3), floatArrayOf(1f, 2f, 4f))
val expected = floatArrayOf(1f, 2.828427f, 8f)
val out = ops.powScalar(a, 1.5f)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun pow_binary_element_wise() {
val a = floatTensor(Shape(4), floatArrayOf(2f, 3f, 4f, 5f))
val b = floatTensor(Shape(4), floatArrayOf(2f, 3f, 0.5f, 1f))
val expected = floatArrayOf(4f, 27f, 2f, 5f)
val out = ops.pow(a, b)
assertCloseTo(expected, (out.data as FloatArrayTensorData<*>).buffer)
}

@Test
fun pow_binary_rejects_shape_mismatch() {
val a = floatTensor(Shape(3), floatArrayOf(1f, 2f, 3f))
val b = floatTensor(Shape(4), floatArrayOf(1f, 2f, 3f, 4f))
assertFailsWith<IllegalArgumentException> { ops.pow(a, b) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,21 @@ internal class RecordingTensorOpsDecorator(private val base: TensorOps) : Tensor
return out
}

// --- Power ops ---
override fun <T : DType, V> pow(a: Tensor<T, V>, b: Tensor<T, V>): Tensor<T, V> {
val out = base.pow(a, b)
record(PowOperation<T, V>(), listOf(a, b), listOf(out))
return out
}

override fun <T : DType, V> powScalar(a: Tensor<T, V>, n: Number): Tensor<T, V> {
val out = base.powScalar(a, n)
// Single-input + scalar exponent stashed in parameters so the
// backward formula can recover it (a-partial is n * a^(n-1)).
record(PowOperation<T, V>(parameters = mapOf("scalar_exponent" to n)), listOf(a), listOf(out))
return out
}

// --- Scalar ops ---
override fun <T : DType, V> addScalar(a: Tensor<T, V>, b: Number): Tensor<T, V> {
val out = base.addScalar(a, b)
Expand Down Expand Up @@ -426,6 +441,9 @@ internal class RecordingTensorOpsDecorator(private val base: TensorOps) : Tensor
override fun <T : DType, V> mean(tensor: Tensor<T, V>, dim: Int?): Tensor<T, V> = base.mean(tensor, dim)
override fun <T : DType, V> variance(tensor: Tensor<T, V>, dim: Int?): Tensor<T, V> = base.variance(tensor, dim)
override fun <T : DType, V> sqrt(tensor: Tensor<T, V>): Tensor<T, V> = base.sqrt(tensor)
override fun <T : DType, V> log(tensor: Tensor<T, V>): Tensor<T, V> = base.log(tensor)
override fun <T : DType, V> log2(tensor: Tensor<T, V>): Tensor<T, V> = base.log2(tensor)
override fun <T : DType, V> log10(tensor: Tensor<T, V>): Tensor<T, V> = base.log10(tensor)
override fun <T : DType, V> abs(tensor: Tensor<T, V>): Tensor<T, V> = base.abs(tensor)
override fun <T : DType, V> sign(tensor: Tensor<T, V>): Tensor<T, V> = base.sign(tensor)
override fun <T : DType, V> clamp(tensor: Tensor<T, V>, minVal: Float, maxVal: Float): Tensor<T, V> = base.clamp(tensor, minVal, maxVal)
Expand Down
Loading
Loading