@@ -14,6 +14,7 @@ import sk.ainet.lang.tensor.ops.TensorSpec
1414import sk.ainet.lang.types.DType
1515import sk.ainet.lang.types.FP32
1616import kotlin.test.Test
17+ import kotlin.test.assertContentEquals
1718import kotlin.test.assertEquals
1819import kotlin.test.assertNotNull
1920import kotlin.test.assertTrue
@@ -119,6 +120,62 @@ class ComputeGraphExecutorTest {
119120 assertNotNull(results[" mm" ])
120121 }
121122
123+ @Test
124+ fun permuteReplaysRecordedAxes () {
125+ // Regression: permute used to be dispatched as a plain last-two-dims
126+ // transpose, silently dropping the recorded axes — wrong for any
127+ // rank-3 permutation (e.g. attention's heads/sequence swap [1, 0, 2]).
128+ var replayedAxes: IntArray? = null
129+ var transposeCalled = false
130+ val ops = object : TensorOps by TestTensorOps () {
131+ override fun <T : DType , V > permute (tensor : Tensor <T , V >, axes : IntArray ): Tensor <T , V > {
132+ replayedAxes = axes
133+ return tensor
134+ }
135+
136+ override fun <T : DType , V > transpose (tensor : Tensor <T , V >): Tensor <T , V > {
137+ transposeCalled = true
138+ return tensor
139+ }
140+ }
141+
142+ val graph = DefaultComputeGraph ()
143+ val input = graph.addNode(inputNode(" input" ))
144+ // The tape records axes as a List<Int> (axes.toList())
145+ val perm = graph.addNode(opNode(" perm" , " permute" , mapOf (" axes" to listOf (1 , 0 , 2 ))))
146+ graph.addEdge(GraphEdge (" e1" , input, perm, tensorSpec = spec()))
147+
148+ val executor = ComputeGraphExecutor (graph, ops)
149+ val rank3 = TestTensor (floatArrayOf(1f , 2f , 3f , 4f , 5f , 6f ), shape = intArrayOf(2 , 1 , 3 ))
150+ val results = executor.execute<FP32 , Float >(mapOf (" input" to rank3))
151+
152+ assertNotNull(results[" perm" ])
153+ assertContentEquals(intArrayOf(1 , 0 , 2 ), replayedAxes, " permute must replay with its recorded axes" )
154+ assertTrue(! transposeCalled, " permute must not degrade to transpose when axes are recorded" )
155+ }
156+
157+ @Test
158+ fun permuteWithoutRecordedAxesFallsBackToTranspose () {
159+ var transposeCalled = false
160+ val ops = object : TensorOps by TestTensorOps () {
161+ override fun <T : DType , V > transpose (tensor : Tensor <T , V >): Tensor <T , V > {
162+ transposeCalled = true
163+ return tensor
164+ }
165+ }
166+
167+ val graph = DefaultComputeGraph ()
168+ val input = graph.addNode(inputNode(" input" ))
169+ val perm = graph.addNode(opNode(" perm" , " permute" ))
170+ graph.addEdge(GraphEdge (" e1" , input, perm, tensorSpec = spec()))
171+
172+ val executor = ComputeGraphExecutor (graph, ops)
173+ val results = executor.execute<FP32 , Float >(mapOf (" input" to TestTensor (floatArrayOf(1f , 2f ))))
174+
175+ assertNotNull(results[" perm" ])
176+ assertTrue(transposeCalled, " legacy traces without axes keep the transpose behavior" )
177+ }
178+
122179 @Test
123180 fun emptyGraphReturnsEmptyResults () {
124181 val graph = DefaultComputeGraph ()
@@ -133,8 +190,11 @@ class ComputeGraphExecutorTest {
133190 * Used for testing graph execution without needing a full tensor backend.
134191 */
135192@Suppress(" UNCHECKED_CAST" )
136- private class TestTensor (val values : FloatArray ) : Tensor<FP32, Float> {
137- private val tensorShape = sk.ainet.lang.tensor.Shape (intArrayOf(1 , values.size))
193+ private class TestTensor (
194+ val values : FloatArray ,
195+ shape : IntArray = intArrayOf(1, values.size),
196+ ) : Tensor<FP32, Float> {
197+ private val tensorShape = sk.ainet.lang.tensor.Shape (shape)
138198 override val data: sk.ainet.lang.tensor.data.TensorData <FP32 , Float > = object : sk.ainet.lang.tensor.data.TensorData <FP32 , Float > {
139199 override fun get (vararg indices : Int ): Float = values[indices.last()]
140200 override fun set (vararg indices : Int , value : Float ) { values[indices.last()] = value }
0 commit comments