Table of contents

Lowering a model written in PowerShell

Kokoro-Hexagon 0a03be39Updated 2026-09-23

src/lower/Lower-Model.ps1. The model description is ordinary PowerShell source. SMA parses it; this walks the AST and recovers a tensor operation DAG. Nothing here executes the model — PowerShell is the authoring language, not the runtime.

Why the AST and not the expression tree

SMA lowers scriptblocks to LINQ expression trees, but those carry dynamic binder call-sites and PSObject boxing — PowerShell's own semantics, which are noise for emitting tensor graphs. The AST is the structure before that lands, and it is a public API.

Result

One AdaIN -> Snake -> Conv pass written as plain PowerShell:

$xm   = Mul $x $mask
$mu   = Mul (ReduceMean $xm -Axis 2) $k
$d    = Sub $x $mu
$sc   = Add (ReduceMax (Abs (Mul $d $mask)) -Axis 2) $tiny
$c    = Div $d $sc
...
$out  = Conv $snake $w $bias -Dilation 1 -Pad 1

lowers to 28 nodes with operands, attributes and consumer counts. Nested calls are hoisted into their own nodes, so (Mul (Abs $d) $m) becomes two nodes and the outer references the inner by id.

Common subexpression elimination removes one node: the source computes Mul $c $mask twice. At full tensor size each removal is a whole pass over memory.

Partitioning maximal runs of pointwise ops whose value has a single consumer:

27 ops -> 14 kernel launches

FUSE  %13:Mul -> %14:Mul -> %15:Div -> %16:Add -> %17:Rsqrt -> %18:Mul -> %19:Mul -> %20:Add -> %21:Mul
FUSE  %22:Mul -> %23:Sin
FUSE  %24:Mul -> %25:Mul -> %26:Add

Partitions break where they must: sc and c have three consumers each and have to materialise, and the reductions and the convolution are their own kernels.

Status

This is a front end and a partitioner. There is no backend that can execute a fused partition. QNN cannot, because its op set is fixed — a partition is only a kernel if we emit the kernel.

That is the measured motivation rather than a preference. r0 emitted as 189 individual QNN ops takes 280 ms against 342 ms for the whole compiled generator, and roughly 520 MB of intermediate traffic accounts for it. Halving the passes is worth more than tiling, which measured as a null inside a real block.

The Qualcomm compiler stays useful as a reference: it produces the oracles we score against, and reading its output gave us the crouton layout, the kernel symbol naming and tile_height=8.