Skip to content

feat(compile): a mixed-precision layer's dtype is a checked preference (#1495) - #1497

Merged
michalharakal merged 1 commit into
developfrom
feature/1495-boundary-dtype-policy
Nov 10, 2026
Merged

michalharakal merged 1 commit into
developfrom
feature/1495-boundary-dtype-policy

Conversation

@michalharakal

Copy link
Copy Markdown
Contributor

Closes #1495. Resolves Open Question 4 of SKEEP-008, the last open one.

Problem

dense(64, dtype = BF16) / stage("head", dtype = FP16) { … } reached a compiled graph as plain convert nodes with narrow-typed ops between them. Nothing recorded what the user declared, so a pass that retypes the graph could change it unnoticed. The issue's first research task confirmed this: DtypeForwardPropagationPass(targetFloatDtype = "BF16") turns an FP16 stage into a BF16 one and no pass objected.

Decision

Of the three options in the issue: metadata on the region, limited to precision boundaries (not a general module scope on every op), with Prefer rather than Require. No callback or strategy parameter.

Change

  • Tagging (skainet-compile-dag): DefaultGraphExecutionContext follows the module call stack through its forward hooks (PrecisionBoundaryScope) and marks every op recorded inside a PrecisionBoundary with DTypePolicy.Prefer(innerType) and the boundary's name (PRECISION_BOUNDARY_METADATA_KEY). The boundary's own two casts are not marked. TraceToGraphBuilder puts both in node metadata, not in operation parameters.
  • Check (skainet-compile-opt): DTypeConstraintResolutionPass checks a boundary's preference against the node's floating-point inputs only and names the boundary:
    Node '…' (matmul) in precision boundary 'head' prefers Float16 but input 0 has dtype 'BF16' — using the existing dtype.
  • PrecisionBoundary.innerType / outerType are public (skainet-lang-core).

Fix found on the way

DTypeConstraintResolutionPass marked a policy-carrying node with removeNode + addNode, and removeNode drops every edge connected to the node. Any dag { … dtypePolicy(…) } graph left the pass with those nodes unwired; with this PR's tagging it would have hit every traced mixed-precision network. The pass now rebuilds the graph with the edges intact, and reports changed = false on an already resolved graph so a multi-iteration pipeline converges.

Review notes

  • DefaultGraphExecutionContext.hooks now wraps the caller's hooks and is never null; the constructor parameter is unchanged. withSchedule passes the caller's hooks on, not the wrapper.
  • Graphs traced without boundaries carry no new metadata (pinned by a test), so existing exports are unchanged.
  • .api files regenerated for lang-core, compile-dag and compile-opt.
  • The diagnostic only appears when the constraint pass runs after the retyping pass; in the default pipelines it runs first.

Tests

  • MixedPrecisionExportTest (+5): ops inside each boundary carry the right policy and the casts, inputs and weights do not; a network without boundaries carries none; the constraint pass is silent on an untouched network and reports the retyped FP16 stage (and not the BF16 dense); cast folding keeps the policies; constraint resolution + cast folding leave the traced graph connected and exportable.
  • DTypeConstraintResolutionPassTest (+3): resolved nodes keep their edges; a second run changes nothing; a boundary policy applies to float inputs only.

Test plan

  • ./gradlew apiCheck checkUnmarkedStubs
  • jvmTest for skainet-compile-opt, skainet-compile-hlo, skainet-compile-dag (after rebase on develop)
  • jvmTest for skainet-lang-core, skainet-lang-models, skainet-backend-cpu and linuxX64 compile of compile-dag / compile-opt (before the rebase)
  • Native and wasm test targets (CI; not run locally)

🤖 Generated with Claude Code

#1495)

A layer or stage declared with a dtype reached a compiled graph as plain
convert nodes with narrow-typed ops between them; nothing recorded what
the user declared. DtypeForwardPropagationPass(targetFloatDtype = "BF16")
turns an FP16 stage into a BF16 one, and no pass objected.

A tracing context now follows the module call stack through its forward
hooks and marks every op recorded inside a PrecisionBoundary with
DTypePolicy.Prefer(innerType) and the boundary's name. The marks are node
metadata, not operation parameters. The boundary's own two casts are not
marked: they run in the boundary itself and have a different dtype on one
side by design. Graphs traced without boundaries are unchanged.

DTypeConstraintResolutionPass checks a boundary's preference against the
node's floating-point inputs only (an index input is not a violation) and
names the boundary in the diagnostic. Prefer, not Require: retyping a
model for a single-dtype target stays allowed, but is no longer silent.

Fix: the pass marked a policy-carrying node by removing and re-adding it,
which dropped every edge connected to that node. It now rebuilds the
graph with the edges intact and reports no change on an already resolved
graph, so a multi-iteration pipeline converges.

PrecisionBoundary.innerType / outerType are public. The tracing context's
hooks wrap the caller's hooks and are never null.

SKEEP-008 Open Question 4 is resolved.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

📖 Documentation Preview

The documentation has been built successfully for this PR.

Generated Files:

  • Operator documentation: docs/modules/operators/_generated_/
  • JSON schema output: operators.json

Artifacts:

  • Download the documentation-preview-1497 artifact to view the complete documentation locally.

This comment will be updated automatically when the PR is updated.

@michalharakal
michalharakal merged commit 8f3446a into develop Nov 10, 2026
20 checks passed
@michalharakal michalharakal mentioned this pull request Nov 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Decide whether a PrecisionBoundary carries DTypePolicy metadata into the compiled graph (SKEEP-008 follow-up)

1 participant