Skip to content

Commit 5e9487d

Browse files
michalharakalclaude
andcommitted
Add toStableHlo(ResolvedComputeGraph) overload (W9 of #615)
Closes the dtype-policy RFC implementation loop: the HLO converter now has an entry point that consumes the resolved graph produced by DTypeConstraintResolutionPass (W7) and validated by the ResolvedComputeGraph wrapper (W8). Contract: callers that flow through this overload get a precondition guarantee — every edge has a parseable dtype, every node carries the `dtype_resolved` marker — before any MLIR string emission. validate=true by default; validate=false escape hatch for debugging. Today the converter still delegates to the underlying ComputeGraph emit path (so HLO output is byte-identical between the two entry points). The wrapper is the *contract*, not a parallel emit. As future passes start writing layout / backend metadata into the resolved graph, the converter can read those typed accessors directly via graph.resolvedLayout / graph.backendAssignment — the hooks are in place. Three tests: - byte-identical output via both entry points for a valid graph - default validate=true rejects an unresolved graph - validate=false escape hatch still emits HLO Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 7806fbb commit 5e9487d

2 files changed

Lines changed: 128 additions & 0 deletions

File tree

skainet-compile/skainet-compile-hlo/src/commonMain/kotlin/sk/ainet/compile/hlo/dag2hlo.kt

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,40 @@ public fun toStableHlo(graph: ComputeGraph, functionName: String = "main"): Stab
5555
return converter.convert(graph, functionName)
5656
}
5757

58+
/**
59+
* Export a [sk.ainet.lang.graph.ResolvedComputeGraph] into a StableHLO
60+
* MLIR module — the dtype-resolved entry point that the W7
61+
* `DTypeConstraintResolutionPass` produces.
62+
*
63+
* The contract this overload upholds vs the plain [ComputeGraph]
64+
* variant: every edge's dtype has already been resolved to a typed
65+
* [sk.ainet.lang.types.DType] (the wrapper's `validate()` would
66+
* have caught any unparseable strings), and every node carries the
67+
* `dtype_resolved` marker from the pass. Callers that flow through
68+
* this overload get a precondition guarantee that the HLO emit
69+
* step won't silently misinterpret a stray dtype string.
70+
*
71+
* Today the converter still consumes the underlying [ComputeGraph] —
72+
* the wrapper is the *contract*, not a separate emit path. As
73+
* future passes start writing layout / backend metadata into the
74+
* resolved graph, the converter can read those typed accessors
75+
* directly. This entry point gives them the stable hook to do so.
76+
*/
77+
public fun toStableHlo(
78+
graph: sk.ainet.lang.graph.ResolvedComputeGraph,
79+
functionName: String = "main",
80+
validate: Boolean = true,
81+
): StableHloModule {
82+
if (validate) {
83+
graph.validate().requireValid()
84+
}
85+
// Delegate to the underlying ComputeGraph emit path — same HLO output
86+
// for graphs that pass validation. Future versions can branch here to
87+
// consume `graph.resolvedLayout(edgeId)` / `graph.backendAssignment(nodeId)`
88+
// once those passes ship.
89+
return toStableHlo(graph.delegate, functionName)
90+
}
91+
5892
/**
5993
* Legacy implementation for backward compatibility.
6094
*
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package sk.ainet.compile.hlo
2+
3+
import kotlin.test.Test
4+
import kotlin.test.assertEquals
5+
import kotlin.test.assertFailsWith
6+
import sk.ainet.lang.graph.DefaultComputeGraph
7+
import sk.ainet.lang.graph.GraphEdge
8+
import sk.ainet.lang.graph.GraphNode
9+
import sk.ainet.lang.graph.ResolvedComputeGraph
10+
import sk.ainet.lang.tensor.ops.GenericOperation
11+
import sk.ainet.lang.tensor.ops.TensorSpec
12+
13+
/**
14+
* Round-trip and byte-equivalence tests for the
15+
* `toStableHlo(ResolvedComputeGraph)` overload added in W9 of #615.
16+
*
17+
* Key property: when the underlying graph passes resolved-graph
18+
* validation, the two HLO entry points must produce byte-identical
19+
* output. The wrapper is the *contract*, not a separate emit path.
20+
*/
21+
class ResolvedComputeGraphHloTest {
22+
23+
private fun buildSimpleGraph(): DefaultComputeGraph {
24+
val g = DefaultComputeGraph()
25+
val resolvedMeta = mapOf<String, Any>("dtype_resolved" to true)
26+
val input = g.addNode(
27+
GraphNode(
28+
id = "input",
29+
operation = GenericOperation("input"),
30+
inputs = emptyList(),
31+
outputs = listOf(TensorSpec("input-out", listOf(2, 3), "FP32")),
32+
metadata = resolvedMeta,
33+
),
34+
)
35+
val relu = g.addNode(
36+
GraphNode(
37+
id = "relu",
38+
operation = GenericOperation("relu"),
39+
inputs = listOf(TensorSpec("input-out", listOf(2, 3), "FP32")),
40+
outputs = listOf(TensorSpec("relu-out", listOf(2, 3), "FP32")),
41+
metadata = resolvedMeta,
42+
),
43+
)
44+
g.addEdge(GraphEdge("e1", input, relu, tensorSpec = TensorSpec("e1", listOf(2, 3), "FP32")))
45+
return g
46+
}
47+
48+
@Test
49+
fun resolved_overload_produces_same_module_as_plain_overload() {
50+
val g = buildSimpleGraph()
51+
val viaPlain = toStableHlo(g)
52+
val viaResolved = toStableHlo(ResolvedComputeGraph(g))
53+
// functionName + content are byte-identical.
54+
assertEquals(viaPlain.functionName, viaResolved.functionName)
55+
assertEquals(viaPlain.content, viaResolved.content)
56+
}
57+
58+
@Test
59+
fun resolved_overload_validates_by_default() {
60+
// Build a graph that's missing the dtype_resolved marker.
61+
val g = DefaultComputeGraph()
62+
val node = g.addNode(
63+
GraphNode(
64+
id = "input",
65+
operation = GenericOperation("input"),
66+
inputs = emptyList(),
67+
outputs = listOf(TensorSpec("input-out", listOf(2), "FP32")),
68+
metadata = emptyMap(),
69+
),
70+
)
71+
// Default validate = true must reject this.
72+
assertFailsWith<IllegalArgumentException> {
73+
toStableHlo(ResolvedComputeGraph(g))
74+
}
75+
}
76+
77+
@Test
78+
fun resolved_overload_skips_validation_when_opted_out() {
79+
// Same unmarked graph, but validate = false.
80+
val g = DefaultComputeGraph()
81+
g.addNode(
82+
GraphNode(
83+
id = "input",
84+
operation = GenericOperation("input"),
85+
inputs = emptyList(),
86+
outputs = listOf(TensorSpec("input-out", listOf(2), "FP32")),
87+
metadata = emptyMap(),
88+
),
89+
)
90+
// No throw expected.
91+
val module = toStableHlo(ResolvedComputeGraph(g), validate = false)
92+
assertEquals("main", module.functionName)
93+
}
94+
}

0 commit comments

Comments
 (0)