SKEEP-004: Virtual tensor layout โ carrying the logical/physical split through the compile pipeline
Status: Draft
Audience: SKaiNET maintainers and contributors; SKaiNET-IREE-tools maintainers (the export contract in improvement 3 concerns them directly)
Created: 2026-08-25
Tracking issue: to be filed
Origin: investigation of develop @ cc2ecc5e against ML Drift (arXiv:2505.00232, "tensor virtualization"); full stage-by-stage evidence with file:line citations in the companion research notes (driftML/virtual-layout-pipeline-findings.md).
Summary
SKEEP-003 gave SKaiNET a complete logical/physical tensor split on the eager
and IO paths: Storage owns bytes, Format says what they mean, Layout
says where element (i,j) lives (strides, offset, block geometry,
BlockOrder), TensorView is what a kernel receives, and WeightForm ร
KernelCapabilities resolve the optimal physical form per device at load
time. This is, independently arrived at, the same design Google’s ML Drift
paper calls tensor virtualization โ decoupling logical tensor indices from
physical storage, with coordinate translation resolved at codegen time and
the physical object chosen per kernel, per device.
The compile pipeline never sees any of it. No file in any skainet-compile
module imports sk.ainet.lang.memory (verified by grep). What crosses the
tape โ DAG โ StableHLO boundaries is a single string โ the encoding name โ
and everything else (strides, block order, module-derived tensor identity,
aliasing) is destroyed in transit. SKEEP-003 knowingly deferred this as phase
P7 ("compiled parity"), which has not started.
This proposal is P7, elevated to its own SKEEP and extended from byte-parity
to layout: capture layout-relevant facts at the tape, decide layout
preferences in a target-parameterized DAG pass, and encode them
structurally into each export exit’s artifact. All of this lives in SKaiNET
core, which owns the whole pipeline โ DSL, tape, DAG, and code generation.
The final physical binding is then made by whatever executes the result:
core’s own eager runtime already does this today (KernelDispatch ร
WeightFormResolver), and for a compiled export it is the consuming backend
โ SKaiNET-IREE-tools for the StableHLO exit today, any future MLIR
derivative or runtime tomorrow. Because the decision pass sits in the
target-neutral DAG IR, every emitter (StableHLO, Minerva, C, JSON, future
dialects) serializes the same metadata; adding a backend never re-plans
layout. Every seam this requires already exists in the code โ several are
pre-named for exactly this feature โ but roughly half of them are dead code
today; the proposal wires them rather than inventing new ones.
Motivation
The eager/compile asymmetry produces recurring, measurable costs:
-
Orientation is guessed at runtime. Weight orientation is not recorded anywhere on the graph, so the fused-op handlers reverse-engineer it by shape-sniffing (
maybeTranspose,LLMFusedOpHandlers.kt:111-119,:181-189), andTransposeEliminationPassstashes eliminated permutations as ad-hoc op parameters (fused_transpose_$i,TransposeEliminationPass.kt:76) that no downstream consumer reads. A virtual layout replaces both: transpose becomes a stride swap (memory/Layout.transpose(),Layout.kt:114). -
The export degrades typed encodings to lossy names. The one metadata key that survives to MLIR is emitted as a string:
TurboQuantPolar(bitsPerElement=4, blockSize=128)exports as"TurboQuant-Polar-4b"(StableHloConverter.kt:84) โ the block size is unrecoverable by any consumer of the.mlir. -
Kernel-feed-order work stops at the eager boundary. #1120 made loading a weight in
KERNEL_FEEDorder a first-class, no-copy mechanism (PackedBlockStorage.blockOrder,rewrapFeedOrderWeight,DefaultCpuOps.kt:1016), and #1118’s acceptance test proves desktop andMOBILE_2GBresolve different forms with identical numbers. None of that reaches an exported model:ExternalParameterRefcarriesencodingbut noBlockOrder/Layout(ConstantMaterialization.kt:91-96), so a feed-ordered weight cannot be blitted into an.irpawith its order declared. -
Constants collapse before the graph exists.
TraceToGraphBuilder.extractFloatArray(TraceToGraphBuilder.kt:364-375) keeps only denseFloatArrayTensorData; a packed weight returnsnulland becomes a bareinputplaceholder. Module-derived identity (TensorId, canonicalmodel.layers[3].attn.q_proj.weight) is not carried byTensorRef(TensorRef.kt:10โ three fields), so no downstream stage can key a per-weight layout policy on a name. -
The designed seams are dead.
TargetOptimizersis an empty registry โ nothing anywhere callsregister/registerDagPasses(TargetOptimization.kt:47-81); thetarget: String?parameter is threaded fromtoStableHlo(dag2hlo.kt:52-70) intoConversionContext.targetand is never set by any caller and never read by any converter;OpGranularityPolicy.keepFusedhas zero call sites;ResolvedComputeGraph.resolvedLayout(edgeId)is a hard-codednullbehind an emptyLayoutmarker interface (ResolvedComputeGraph.kt:54,111), with the consuming branch point pre-named atdag2hlo.kt:101-105. Production callers bypassskainet-compile-optentirely (HloCompiler.kt:95-101,BackendManager.kt:216-221,HloGenerator.kt:55).
For calibration: ML Drift’s GPU runtime derives much of its reported performance from selecting the optimal physical tensor object per kernel at initialization, using device-specific empirical data, behind a stable logical indexing scheme. SKaiNET has more insertion points than ML Drift’s single runtime โ tape, DAG, emission, and an external compiler it also controls โ which is precisely why the layout decision should be threaded through the pipeline rather than frozen at any single stage.
Current State
The seam inventory, live path vs designed path, as of cc2ecc5e:
| Seam | Where | Status |
|---|---|---|
|
|
Live โ already carries |
|
|
Live โ the only metadata that crosses all stages |
|
|
Live โ but name-strings only; comments self-described as a stopgap |
|
|
Live โ blits quantized bytes verbatim; carries no layout; uses legacy
|
|
|
Dead โ registry never registered into; module has no production dependents |
|
|
Dead โ never set, never read; |
|
|
Dead โ threaded, zero call sites |
|
|
Stub โ hard-coded |
|
|
Stub โ declaration with zero usages |
The IREE compiler itself is not invoked from this repository โ verified: no
iree-compile call, binding, task, or script exists; the invocation lives in
SKaiNET-IREE-tools. Core’s handoff is the .mlir text plus the .irpa
archive, and by design "the archive has no dtype, no shape โฆ the MLIR
#flow.parameter.named reference carries all structural metadata"
(IrpaWriter.kt:36-38). Any layout information must therefore ride the
MLIR. That file is the contract.
Proposed Design
Stage responsibilities
"Ownership" here means which pipeline stage is positioned to make which decision โ it says nothing about repositories or teams; every stage below except the last is SKaiNET core. The design question is not which single stage decides layout โ each stage holds facts no other stage has:
| Stage | Verb | Rationale |
|---|---|---|
Tape |
Captures, doesn’t decide |
The only stage that can reach the live tensor: its |
Graph / DAG |
Decides preferences |
The only stage with fusion groupings, def-use with port indices, and
orientation intent. Decisions are target-parameterized (via
|
Code generation (per export exit) |
Encodes, doesn’t decide |
Each emitter serializes the same decided layout in its own format: the
StableHLO exit as MLIR attributes (module attribute first, per-type
attribute later) plus |
Consuming backend / runtime |
Finalizes physical |
Whatever executes the result holds the empirical device facts and makes
the final binding โ ML Drift’s per-kernel, per-device physical-object
choice. Core’s own eager runtime already does this ( |
One vocabulary, not three
sk.ainet.lang.memory.Layout becomes the single layout value type across the
pipeline. Today three unrelated notions share the concept: the real
memory.Layout, the empty sk.ainet.lang.graph.Layout marker, and the
runtime KernelKey.LayoutClass. The graph marker is made an alias for (or is
implemented by) memory.Layout so resolvedLayout(edgeId) finally has a
real return type; LayoutClass remains the dispatch-key projection of it.
skainet-compile-dag already depends on skainet-lang-core, so no new
module edge. (SKEEP-003 decision 2 already marked graph.Layout as
deprecated-when-replacement-lands; this is that replacement.)
The seven data flows
-
Tape โ graph: identity and physical facts. Extend
TensorRef(TensorRef.kt:10โ the narrowest bottleneck in the pipeline) withtensorId: TensorId?,encoding: TensorEncoding?, and optionallayout: Layout?, populated inTraceSession.refOffrom the live tensor.TraceToGraphBuilder.buildInputSpecs/buildOutputSpecs(:340-362) โ which today drop all metadata โ write them intoTensorSpec.metadata. -
The layout key. New
TensorSpecLayout.ktbesideTensorSpecEncoding.kt, copied verbatim in shape:TENSOR_LAYOUT_METADATA_KEY,val TensorSpec.tensorLayout: Layout?,fun TensorSpec.withTensorLayout(โฆ). Zero schema change; survives every existing pass automatically (they allcopy()). -
The decision pass.
LayoutAssignmentPass : GraphOptimizationPassinskainet-compile-opt, registered per target viaTargetOptimizers.registerDagPasses(target) { โฆ }. Ordering: afterTransposeEliminationPass(absorbtransposeA/Bandfused_transpose_$iinto strides), beforeLLMFusionPass(fusion erases the intermediates a layout pass needs). -
Consume the stub. Populate
ResolvedComputeGraph.resolvedLayoutfrom the pass result and branch atdag2hlo.kt:101-105โ the comment already names this as the intended consumption point. -
Serialize structurally. Phase 1: a
skainet.tensor_layoutsmodule attribute mirroringcollectTensorEncodings(StableHloConverter.kt:81-88,251-264) โ carrying strides,blockOrderand block geometry as values, nevername-strings (the existing precedent’s lossiness is the counterexample). Phase 2: real per-type encoding attributes (tensor<NxMxf32, #layout>) via theTypeMapper.mapTensorTypechoke point (TypeMapper.kt:53-58,135-139, 154-158). -
Layout on external weights. Add
layout: Layout?(at minimumblockOrder) toExternalParameterRefsoIrpaWritercan blit a kernel-feed-order weight verbatim with its order declared in the MLIR reference. Longer term, migrate itssourcefrom legacyBufferHandletoStorage(that is SKEEP-003 P7/P8 territory). -
Target, for real. Thread
targetintoStableHloConverterFactory.createExtended/createFast/createCustom(onlycreateBasicaccepts it today), add--targettoHloGeneratorMainand thegenerateHloGradle task, and readConversionContext.targetwhere emission differs per device. NoteGraphExportContext.targetNameis currently misused as the MLIR function name (StableHloGraphExport.kt:40); Minerva’s convention (metadata["target"],MinervaGraphCanonicalizer.kt:429) is the model to follow or the misuse to fix.
Design Constraint โ the export artifact is the contract
For the StableHLO exit specifically, the MLIR carries the contract (the
.irpa deliberately has no structural metadata); other exits carry the same
metadata in their own formats. Two constraints bind every slice:
-
Structural serialization only. A layout attribute carries strides, block order, and block geometry as typed values with a stable string form. The
encoding.namedegradation ("TurboQuant-Polar-4b"losing its block size) is the anti-pattern this proposal exists to not repeat. The stable string form also protects the Minerva path, which stringifies all spec metadata (MinervaGraphCanonicalizer.kt:341). -
Preferences in core, final binding in the consumer. Core annotates; each consuming backend is free to relayout for its device without forcing a re-export, which keeps core honest about not owning hardware facts it does not have. For the StableHLO exit, the attribute schema is agreed with SKaiNET-IREE-tools before per-type emission lands (slice 2 below is that agreement).
As in SKEEP-003: the packed-encoding system must survive bit-identically. The golden parity tests (packed matmul outputs, StableHLO encoding attributes) gate every slice.
Compatibility and Migration
The house rule holds: deprecate, don’t delete; additive with defaults that reproduce historical behavior.
-
TensorRefgains fields withnulldefaults โ every existing constructor call compiles and behaves identically; the tape format is additive. -
TensorSpecis untouched โ the layout rides the existingmetadatamap exactly astensorEncodingdoes. -
skainet.tensor_layoutsis a new module attribute; consumers that ignore it see today’s MLIR. Per-type attributes (slice 4) are the only output-changing step and land behind the--target/policy opt-in. -
sk.ainet.lang.graph.Layout(empty marker) is deprecated withReplaceWithper SKEEP-003 decision 2;resolvedLayoutkeeps its signature. -
Wiring
dagPipelineForinto production (slice 3) initially registers an empty per-target pass list, so behavior is unchanged until a target opts in.
Implementation Slices
Each slice is a normal PR that stands on its own; ordered so pure plumbing lands first with zero behavior change and the risky converter work comes last, after the export contract is agreed.
| Slice | What | Files |
|---|---|---|
1 โ Carry identity and layout to the graph |
Extend |
|
2 โ Layout on the export boundary |
Emit |
|
3 โ First real decision pass, one target |
Wire |
new |
4 โ Per-type MLIR attributes |
|
|
Rollout Plan
-
Maintainer discussion on this SKEEP settles the open questions, principally the attribute schema ownership with SKaiNET-IREE-tools.
-
Slice 1 lands (pure plumbing, trace-roundtrip test green).
-
Slice 2 lands together with a schema note in SKaiNET-IREE-tools; a
.vmfbround-trip there is the acceptance gate. -
Slice 3 lands per target, starting with the CPU/
llvm-cpupath whereKERNEL_FEEDweights already exist end-to-end on the eager side. -
Slice 4 proceeds only after the converter-literal audit, behind the target opt-in.
Acceptance Criteria
-
A traced model with a
KERNEL_FEED-loaded Q4_K weight produces aComputeGraphwhere that weight’sTensorSpecreports itsTensorId, encoding, andblockOrder(slice 1, roundtrip test). -
The exported
.mlirfor that model carries askainet.tensor_layoutsentry from which strides/blockOrder/block-size are recoverable as values, and the.irpaentry for the weight declares its order (slice 2); SKaiNET-IREE-tools compiles it to a.vmfbwhose outputs match the eager path (the SKEEP-003 P7 parity run). -
With the layout pass enabled, no fused-op handler calls
maybeTransposeshape-sniffing for a weight whose orientation the graph declares (slice 3). -
Golden parity: packed matmul outputs and existing
skainet.tensor_encodingsattributes are bit-identical before vs after every slice;generateHlooutput without--targetis byte-identical.
GPU Validation
ML Drift demonstrates its results on GPUs โ 5-11x token-prefill speedup on
Qualcomm Adreno against open-source LLM runtimes, with "weights conversion
with optimal memory layout" named among the key transformations. This
proposal’s mechanism reaches GPUs without any change to core: the same
.mlir + .irpa export goes to SKaiNET-IREE-tools with a GPU target
(vulkan-spirv for Adreno/Mali, Metal for Apple, CUDA/ROCm on desktop),
and the layout preference rides the same metadata.
The docking point on the IREE side is concrete: IREE’s layout machinery
(data-tiling) is driven by encoding attributes, and explicitly supports a
data-tiling hint attribute attached to matmuls in a preprocessing phase or
in the model itself โ the pass skips ops that already carry the hint. So
SKaiNET-IREE-tools translates skainet.tensor_layouts into IREE’s native
encoding hints during preprocessing rather than fighting IREE’s planner.
GPU data-tiling for LLM workloads is an active, in-progress IREE effort
(iree-org/iree#21195), which means the defaults are young and external
hints have headroom to matter.
Hypotheses where a measurable advantage is plausible, ranked:
-
Mobile-GPU prefill (Adreno/Mali) โ the compute-bound matmul phase where ML Drift’s layout-driven gains concentrate, and the bandwidth/cache-sensitive GPU class where physical layout matters most.
-
Load time and time-to-first-token via pre-tiled
.irpaโ IREE’s compile-time data-tiling repacks constant weights; externalized parameters are the awkward case. Shipping weights already in the layout the GPU dispatch wants, declared in the MLIR reference, avoids a runtime repack โ the GPU analog of #1120’s no-copyKERNEL_FEEDrewrap on CPU. -
Per-device-class selection โ profile-driven layout preferences per GPU family, through the same
PlannerProfileรKernelCapabilitiesresolution machinery that already selects weight forms per device class.
Experiment design. The benchmark that isolates this proposal’s
contribution is IREE-default vs IREE-with-hints, A/B on the same device โ
not a comparison against ML Drift itself, whose hand-tuned OpenCL kernels
use vendor extensions and texture-cache techniques that layout hints alone
cannot express. Setup: Llama-3.2-1B Q4 (the SKEEP-003 acceptance model) on
three device classes โ an Adreno-class embedded board (vulkan-spirv; the
Arduino Ventuno Q with its Dragonwing IQ8 / Adreno A623 is on order as the
candidate platform โ hardware/software notes in the research workspace), an
Apple M-series machine (Metal), and one desktop GPU. Metrics: prefill
tok/s, decode tok/s, time-to-first-token, model load time, peak memory.
Success criterion: a reproducible win on at least one GPU class.
Known unknowns. GGML block formats (Q4_K) are not native to IREE โ the
export re-encodes to an IREE-friendly int4+scales layout, which
WeightForm’s `EncodingRequest already models conceptually. Embedded
Adreno driver quality for Vulkan compute (or OpenCL) on Linux varies and
gates the whole experiment; a driver smoke test precedes any layout work
on the candidate board.
Risks
-
The opt pipeline is dead code today โ slice 3’s wiring is a prerequisite for any pass-based design doing anything; until then the registry pattern is inert by construction.
-
Fused ops have no HLO converters โ running
createLLM()fusion beforetoStableHloyields MLIR with// Unsupported opholes. This SKEEP deliberately does not couple to enabling the fusion pipeline. -
Regex-based text passes โ
StableHloOptimizerandMlirParsertruncate at the first>inside a nested attribute; they must be fixed or kept disabled before slice 4. -
Metadata stringification on the Minerva path โ a typed
Layoutwithout a stable string form is destroyed atMinervaGraphCanonicalizer.kt:341. -
dtype alias chaos โ
"FP32"/"F32"/"Float32"all occur andTraceSession.refOfsilently degrades unknown dtypes to FP32 (TraceSession.kt:25-34); layout logic keyed on dtype must route through the existing alias tables.
Open Questions
-
Who owns the attribute schema โ this repo or SKaiNET-IREE-tools? The proposal assumes a jointly-versioned schema note living beside the
.irpaformat doc; the alternative (core-owned, tools-follows) couples releases. -
TensorRefextension vs metadata-only capture โ extending the data class is the highest-leverage single change but touches the trace format; the fallback is capturing everything inOpTrace.attributesand joining inTraceToGraphBuilder. The proposal prefers the former for the same reasonTensorRefexists at all: it is the typed bottleneck. -
Preference strength โ is a DAG-assigned layout a hint the driver may ignore silently, or a requirement whose violation is diagnosed? ML Drift argues for hint-plus-empirical-override;
strictprofiles (SKEEP-003 decision 11) argue some deployments want hard failure. -
Does
stableHloPasses()(the reservedCompilePhase.STABLE_HLOhook) get defined in this SKEEP or stay reserved? Slice 4 can be done either in the converter or as a late pass; the proposal leans converter-side and leaves the hook reserved.
References
-
Parent: SKEEP-003 โ this proposal is its P7 phase, elevated and extended from parity to layout; decisions 2 (naming/deprecations) and 11 (planner profiles) bind here.
-
As-built documentation: The memory model, Packed weight layout.
-
ML Drift: arXiv:2505.00232 ("Scaling On-Device GPU Inference for Large Generative Models") โ tensor virtualization; per-kernel, per-device physical object selection; 5-11x Adreno prefill speedups with optimal weight memory layout.
-
IREE data-tiling: walkthrough (encoding attributes, preprocessing-phase hints); iree#21195 (GPU data-tiling for llama, in progress).
-
Related issues: #1109 (weight form, slices 1-5), #1118 (weight-form acceptance), #1120 (kernel feed order), #1124 (blockOrder plumbing into views), #968/#971/#973 (the wrong-block bugs that hardened
BlockOrder). -
External: SKaiNET-IREE-tools (the compiler driver; consumer of the export contract in slice 2).
-
Evidence base: stage-by-stage pipeline investigation with file:line citations, develop @
cc2ecc5e(driftML/virtual-layout-pipeline-findings.mdin the research workspace).