Skip to content

Commit b2a1e16

Browse files
committed
compile: add target + op-granularity plugin seam to StableHLO emitter
Thread an optional target id and OpGranularityPolicy through toStableHlo -> StableHloConverterFactory -> StableHloConverter -> ConversionContext, so per-target emission decisions (fused vs decomposed ops = target legalization) are possible without any hardware knowledge in the agnostic core. OpGranularityPolicy (+ FusedOpAllowList) lives in skainet-compile-dag, the module both the emitter (skainet-compile-hlo) and the TargetOptimizer registry (skainet-compile-opt) already depend on, so no new dependency edges and no cycle. TargetOptimizer gains granularity(); TargetOptimizers gains granularityFor(target). The caller resolves the policy and passes it in, keeping the emitter decoupled from the registry. Everything defaults to null (decompose all), so emission is byte-identical today.
1 parent 006c5ab commit b2a1e16

6 files changed

Lines changed: 94 additions & 8 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package sk.ainet.compile.target
2+
3+
/**
4+
* Per-target policy deciding, for a **fused** graph op (by op name, e.g. "layernorm",
5+
* "scaleddotproductattention"), whether an emitter should KEEP it as a single target op
6+
* (`stablehlo.composite` / native kernel-call) or DECOMPOSE it into primitives.
7+
*
8+
* This is the seam for **target legalization** — matching the op *granularity* a fragile
9+
* vendor compiler expects, without leaking any hardware knowledge into the agnostic core.
10+
* The core never calls this directly; the emitters ([toStableHlo] / the C codegen) consult
11+
* the policy their caller resolved for the selected target. A `null` policy means "decompose
12+
* everything", which is the portable default (llvm-cpu et al.).
13+
*
14+
* Lives in the shared DAG module so both the StableHLO emitter (`skainet-compile-hlo`) and
15+
* the `TargetOptimizer` registry (`skainet-compile-opt`) can reference it without either
16+
* gaining a new dependency edge.
17+
*/
18+
public interface OpGranularityPolicy {
19+
/** Backend/device id this policy applies to (matches the target string / iree device). */
20+
public val target: String
21+
22+
/** `true` = keep [opName] fused (emit composite / kernel-call); `false` = decompose. */
23+
public fun keepFused(opName: String): Boolean
24+
}
25+
26+
/**
27+
* Straightforward allow-list policy: an op is kept fused iff its (lower-cased) name is in
28+
* [fused]. Everything else decomposes.
29+
*/
30+
public class FusedOpAllowList(
31+
override val target: String,
32+
private val fused: Set<String>,
33+
) : OpGranularityPolicy {
34+
override fun keepFused(opName: String): Boolean = opName.lowercase() in fused
35+
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,19 @@ public class ConversionContext @kotlin.jvm.JvmOverloads constructor(
2323
* See issue #523 for the architecture context.
2424
*/
2525
public val materializationPolicy: ConstantMaterializationPolicy =
26-
ConstantMaterializationPolicy.InlineAlways
26+
ConstantMaterializationPolicy.InlineAlways,
27+
/**
28+
* Selected compile target (iree device id, e.g. "torq"); `null` = target-agnostic.
29+
* Threaded through so per-target emit decisions (e.g. op granularity) are possible
30+
* without any hardware knowledge in the shared emitter.
31+
*/
32+
public val target: String? = null,
33+
/**
34+
* Per-target op-granularity policy (fused vs decomposed emission). Resolved by the
35+
* caller from the [sk.ainet.compile.opt.TargetOptimizers] registry and passed in;
36+
* `null` = decompose everything (portable default). The emitter only *reads* it.
37+
*/
38+
public val granularity: sk.ainet.compile.target.OpGranularityPolicy? = null
2739
) {
2840
private val valueNames = mutableMapOf<String, String>()
2941
private val valueTypes = mutableMapOf<String, String>()

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,18 @@ public class StableHloConverter @kotlin.jvm.JvmOverloads constructor(
2323
* through to every [ConversionContext] this converter creates.
2424
*/
2525
private val materializationPolicy: ConstantMaterializationPolicy =
26-
ConstantMaterializationPolicy.InlineAlways
26+
ConstantMaterializationPolicy.InlineAlways,
27+
/** Selected compile target (iree device id, e.g. "torq"); handed to every context. */
28+
private val target: String? = null,
29+
/** Per-target op-granularity policy; handed to every context (null = decompose all). */
30+
private val granularity: sk.ainet.compile.target.OpGranularityPolicy? = null
2731
) {
2832

2933
/**
3034
* Convert a ComputeGraph to StableHLO MLIR format
3135
*/
3236
public fun convert(graph: ComputeGraph, functionName: String = "main"): StableHloModule {
33-
val context = ConversionContext(typeMapper, graph, materializationPolicy)
37+
val context = ConversionContext(typeMapper, graph, materializationPolicy, target, granularity)
3438

3539
// Pre-conversion validation (allow orphaned nodes for backward compatibility)
3640
val validationResult = graph.validate()

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,9 @@ public object StableHloConverterFactory {
3232
@JvmStatic
3333
@kotlin.jvm.JvmOverloads
3434
public fun createBasic(
35-
policy: ConstantMaterializationPolicy = ConstantMaterializationPolicy.InlineAlways
35+
policy: ConstantMaterializationPolicy = ConstantMaterializationPolicy.InlineAlways,
36+
target: String? = null,
37+
granularity: sk.ainet.compile.target.OpGranularityPolicy? = null
3638
): StableHloConverter {
3739
val registry = StableHloOperationRegistry()
3840
val typeMapper = TypeMapper()
@@ -75,7 +77,7 @@ public object StableHloConverterFactory {
7577
// LLM front-door op for token-id \u2192 embedding lookups.
7678
registry.register(GatherOperationsConverter())
7779

78-
return StableHloConverter(registry, typeMapper, validator, policy)
80+
return StableHloConverter(registry, typeMapper, validator, policy, target, granularity)
7981
}
8082

8183
/**

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

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,23 @@ public data class StableHloModule(
4949
* - Currently supports: input, add, matmul, relu. Unsupported ops are emitted as comments.
5050
* - DType mapping expects TensorSpec.dtype to be strings like "FP32", "F32", "F64", "I32".
5151
*/
52-
public fun toStableHlo(graph: ComputeGraph, functionName: String = "main"): StableHloModule {
52+
public fun toStableHlo(
53+
graph: ComputeGraph,
54+
functionName: String = "main",
55+
/**
56+
* Selected compile target (iree device id, e.g. "torq", "llvm-cpu"); `null` =
57+
* target-agnostic emission (the portable default, unchanged behavior).
58+
*/
59+
target: String? = null,
60+
/**
61+
* Per-target op-granularity policy (fused vs decomposed). Resolve it at the call
62+
* site with `TargetOptimizers.granularityFor(target)` and pass it in, so the emitter
63+
* stays decoupled from the optimizer registry. `null` = decompose everything.
64+
*/
65+
granularity: sk.ainet.compile.target.OpGranularityPolicy? = null,
66+
): StableHloModule {
5367
// Use the new converter architecture
54-
val converter = StableHloConverterFactory.createBasic()
68+
val converter = StableHloConverterFactory.createBasic(target = target, granularity = granularity)
5569
return converter.convert(graph, functionName)
5670
}
5771

@@ -78,6 +92,8 @@ public fun toStableHlo(
7892
graph: sk.ainet.lang.graph.ResolvedComputeGraph,
7993
functionName: String = "main",
8094
validate: Boolean = true,
95+
target: String? = null,
96+
granularity: sk.ainet.compile.target.OpGranularityPolicy? = null,
8197
): StableHloModule {
8298
if (validate) {
8399
graph.validate().requireValid()
@@ -86,7 +102,7 @@ public fun toStableHlo(
86102
// for graphs that pass validation. Future versions can branch here to
87103
// consume `graph.resolvedLayout(edgeId)` / `graph.backendAssignment(nodeId)`
88104
// once those passes ship.
89-
return toStableHlo(graph.delegate, functionName)
105+
return toStableHlo(graph.delegate, functionName, target, granularity)
90106
}
91107

92108
/**

skainet-compile/skainet-compile-opt/src/commonMain/kotlin/sk/ainet/compile/opt/TargetOptimization.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ public interface TargetOptimizer {
2626
/** DAG-phase graph rewrites (produce standard, still-portable ops). */
2727
public fun dagPasses(): List<GraphOptimizationPass> = emptyList()
2828

29+
/**
30+
* Op-granularity policy for the emitters (fused vs decomposed). `null` = decompose
31+
* everything (the portable default). A target that wants a fused op kept as a single
32+
* `stablehlo.composite` / kernel-call returns a policy here. See
33+
* [sk.ainet.compile.target.OpGranularityPolicy].
34+
*/
35+
public fun granularity(): sk.ainet.compile.target.OpGranularityPolicy? = null
36+
2937
// Future phases keep the same shape, e.g.:
3038
// public fun tapePasses(): List<TapePass> = emptyList()
3139
// public fun stableHloPasses(): List<StableHloPass> = emptyList()
@@ -59,6 +67,15 @@ public object TargetOptimizers {
5967
public fun forTarget(target: String?): List<TargetOptimizer> =
6068
target?.let { registry[it]?.toList() } ?: emptyList()
6169

70+
/**
71+
* The op-granularity policy for [target] — the first non-null [TargetOptimizer.granularity]
72+
* among the registered optimizers, or `null` (decompose everything) if none provide one.
73+
* Callers (the model-build tool) resolve this and pass it into `toStableHlo(...)`, so the
74+
* emitter stays decoupled from this registry.
75+
*/
76+
public fun granularityFor(target: String?): sk.ainet.compile.target.OpGranularityPolicy? =
77+
forTarget(target).firstNotNullOfOrNull { it.granularity() }
78+
6279
/** Test/reset hook. */
6380
public fun clear(): Unit = registry.clear()
6481
}

0 commit comments

Comments
 (0)