Skip to content

feat(language): add pl.graph, the in-place form of @pl.jit.graph - #2628

Merged
Hzfengsy merged 1 commit into
hw-native-sys:mainfrom
lyfne123:feat/hbg-p3-graph-scope-form
Sep 4, 2026
Merged

feat(language): add pl.graph, the in-place form of @pl.jit.graph#2628
Hzfengsy merged 1 commit into
hw-native-sys:mainfrom
lyfne123:feat/hbg-p3-graph-scope-form

Conversation

@lyfne123

@lyfne123 lyfne123 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

What

with pl.graph("name"): marks a repeated orchestration region as a recordable
graph in place, instead of requiring it to be lifted into its own
@pl.jit.graph function first.

@pl.jit
def decode(w: pl.Tensor, hidden: pl.InOut[pl.Tensor]):
    for layer in pl.range(40):
        with pl.graph("decoder_layer"):     # recorded once, replayed 39 times
            ...
    return hidden

Why

The decorator form is the wrong shape when the region is a slice of a larger
orchestration body. Lifting it is mechanical work that invents a name and a
parameter list the author did not want, and it turns a one-line annotation into
an unreviewable diff. The two surfaces are now an ergonomic choice, not a
semantic one: the decorator suits a layer that is already its own function, the
scope suits a region the author would rather leave where it is.

How

A new OutlineGraphScopes pass lifts each GraphScopeStmt back out into a
FunctionType::Graph function named after the region, leaving a Call behind.

It runs immediately before OutlineIncoreScopes, which is the whole design:
the pl.at scopes inside a marked region are then outlined on exactly the same
terms as those inside a hand-written Graph function, and LegalizeGraphBoundary,
the Graph verifier and orchestration codegen see one representation rather than
two. Placing it there also keeps the new node alive across three passes instead
of forty — the RFC identifies that reach as the real cost of a scope-shaped
carrier.

Parameter order is the one thing that does not carry over: the outliner
appends in capture order while the decorator form uses the declared signature.
The two boundaries are permutations of each other, and nothing downstream reads
a boundary parameter by position.

The region name is required

Unlike every other scope kind's optional name_hint, pl.graph demands one. It
becomes the outlined function's name, hence the emitted symbol, hence the
runtime's graph key — an auto-generated name would silently change a recorded
graph's identity whenever an unrelated region was added earlier in the file. Two
regions asking for the same name are disambiguated rather than merged; sharing
one name would give two topologies a single GraphDefinition and make the second
call replay the first one's graph.

Rejected at compile time, not degraded silently

The runtime's answer to each of these is to fall back to ordinary submits: correct
numbers, no diagnostic, and the feature quietly stops paying. So each is a
compile error instead — a Graph region nested inside another Graph region, inside
pl.at / pl.cluster / pl.spmd (those become one device task, whereas a Graph
region records a topology of them), written in a device kernel body, or given no
name.

Pass renumbering

Inserting the pass at pipeline position 8 renumbers pass docs 08-49 → 09-50
(84 files, EN + ZH) per .claude/rules/pass-doc-ordering.md, with every
cross-reference updated. The renamed files' contents differ only in doc-number
references.

Testing

  • tests/ut/ir/statements/test_graph_scope_stmt.py — node construction,
    structural equality, .pto round-trip, print → reparse fixpoint
  • tests/ut/ir/transforms/test_outline_graph_scopes.py — outlining, structural
    equivalence with the decorator form, name disambiguation, the loop case, the
    OutlineIncoreScopes hand-off, and every rejection path
  • tests/st/runtime/framework_and_models/test_graph_execution.py — a scope-form
    device case sharing test_per_layer_accumulate's golden (a frozen per-layer
    offset gives 4.0 instead of 10.0), plus compile-side assertions that the region
    really lowers to a Graph and presents the same boundary arity as the decorator
    form

Full UT suite green. clang-tidy (full-tree, since CMakeLists.txt changed),
clang-format, ruff, pyright, markdownlint, and the repository's
check_ir_property_parity / check_docs_nav / check_docs_en_zh_parity /
check_headers / check_english_only / check_no_broad_raises lints all pass.

The device cases in test_graph_execution.py were not run here — that file needs
hardware and --forked; CI owns it.

Scope

This is the with pl.graph(...) half of RFC #2399's P3. The other half, scratch-arena
packing when a boundary exceeds 32 tensors, is not included and appears to be
obsolete: the runtime's cap is now GRAPH_MAX_TENSOR_ARGS = 128 /
GRAPH_MAX_SCALAR_ARGS = 64, which LegalizeGraphBoundary already encodes, so a
decoder layer with twenty-odd weights sits far under it. Worth confirming before
that item is revived.

Related: #2399

@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 215 files, which is 115 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: c4b4f330-3a80-425d-ae39-0e873c597cd6

📥 Commits

Reviewing files that changed from the base of the PR and between b816516 and e2a70f1.

📒 Files selected for processing (215)
  • .claude/rules/pass-doc-ordering.md
  • CMakeLists.txt
  • docs/en/dev/07-memory-map.md
  • docs/en/dev/codegen/00-pto_codegen.md
  • docs/en/dev/codegen/01-orchestration_codegen.md
  • docs/en/dev/debug/00-torch_codegen.md
  • docs/en/dev/distributed_ops.md
  • docs/en/dev/ir/01-hierarchy.md
  • docs/en/dev/ir/02-types.md
  • docs/en/dev/ir/05-operators.md
  • docs/en/dev/ir/08-param-directions.md
  • docs/en/dev/ir/09-multi_output_ops.md
  • docs/en/dev/language/00-python_syntax.md
  • docs/en/dev/language/01-statements.md
  • docs/en/dev/language/02-manual_dependencies.md
  • docs/en/dev/language/05-cache-policy.md
  • docs/en/dev/passes/00-pass_manager.md
  • docs/en/dev/passes/01-inline_functions.md
  • docs/en/dev/passes/05-simplify.md
  • docs/en/dev/passes/08-outline_graph_scopes.md
  • docs/en/dev/passes/09-outline_incore_scopes.md
  • docs/en/dev/passes/10-outline_cluster_scopes.md
  • docs/en/dev/passes/11-convert_tensor_to_tile_ops.md
  • docs/en/dev/passes/12-optimize_orch_tensors.md
  • docs/en/dev/passes/13-lower_composite_ops.md
  • docs/en/dev/passes/14-flatten_tile_nd_to_2d.md
  • docs/en/dev/passes/15-block_nz_tensor_views.md
  • docs/en/dev/passes/16-legalize_tile_cast.md
  • docs/en/dev/passes/17-auto_tile_matmul_l0.md
  • docs/en/dev/passes/18-canonicalize_tile_slice.md
  • docs/en/dev/passes/19-infer_tile_memory_space.md
  • docs/en/dev/passes/20-insert_mx_scale_addr.md
  • docs/en/dev/passes/21-resolve_backend_op_layouts.md
  • docs/en/dev/passes/22-lower_auto_vector_split.md
  • docs/en/dev/passes/23-expand_mixed_kernel.md
  • docs/en/dev/passes/24-inject_gm_pipe_buffer.md
  • docs/en/dev/passes/25-split_vector_kernel.md
  • docs/en/dev/passes/26-stamp_tfree_split.md
  • docs/en/dev/passes/27-normalize_return_order.md
  • docs/en/dev/passes/28-skew_cross_core_pipeline.md
  • docs/en/dev/passes/29-lower_pipeline_to_slots.md
  • docs/en/dev/passes/30-lower_pipeline_loops.md
  • docs/en/dev/passes/31-canonicalize_io_order.md
  • docs/en/dev/passes/32-materialize_tensor_strides.md
  • docs/en/dev/passes/33-init_memref.md
  • docs/en/dev/passes/34-materialize_semantic_aliases.md
  • docs/en/dev/passes/35-memory_reuse.md
  • docs/en/dev/passes/36-allocate_memory_addr.md
  • docs/en/dev/passes/37-fold_no_op_reshape.md
  • docs/en/dev/passes/38-fuse_create_assemble_to_slice.md
  • docs/en/dev/passes/39-derive_call_directions.md
  • docs/en/dev/passes/40-auto_derive_task_dependencies.md
  • docs/en/dev/passes/41-expand_manual_phase_fence.md
  • docs/en/dev/passes/42-synthesize_allreduce_signals.md
  • docs/en/dev/passes/43-materialize_comm_domain_scopes.md
  • docs/en/dev/passes/44-lower_host_tensor_collectives.md
  • docs/en/dev/passes/45-materialize_dist_tensor_ctx.md
  • docs/en/dev/passes/46-legalize_graph_boundary.md
  • docs/en/dev/passes/47-materialize_runtime_scopes.md
  • docs/en/dev/passes/48-classify_iter_arg_carry.md
  • docs/en/dev/passes/49-insert_comm_fence.md
  • docs/en/dev/passes/50-materialize_valid_shape_symbols.md
  • docs/en/dev/passes/99-verifier.md
  • docs/en/dev/passes/index.md
  • docs/en/reference/pto-isa/index.md
  • docs/en/user/index.md
  • docs/en/user/language/00-types.md
  • docs/en/user/language/01-functions.md
  • docs/en/user/language/02-control-flow.md
  • docs/en/user/language/03-memory.md
  • docs/en/user/language/04-scopes.md
  • docs/en/user/ops/00-dispatch.md
  • docs/en/user/ops/01-catalog.md
  • docs/en/user/precision/00-workflow.md
  • docs/en/user/precision/01-cases.md
  • docs/en/user/precision/index.md
  • docs/en/user/tasks/00-model.md
  • docs/en/user/tasks/01-scopes.md
  • docs/en/user/tasks/index.md
  • docs/en/user/tools/02-memory-map.md
  • docs/en/user/tutorials/03-mixed-kernel.md
  • docs/zh/dev/07-memory-map.md
  • docs/zh/dev/codegen/00-pto_codegen.md
  • docs/zh/dev/codegen/01-orchestration_codegen.md
  • docs/zh/dev/debug/00-torch_codegen.md
  • docs/zh/dev/distributed_ops.md
  • docs/zh/dev/ir/01-hierarchy.md
  • docs/zh/dev/ir/02-types.md
  • docs/zh/dev/ir/05-operators.md
  • docs/zh/dev/ir/08-param-directions.md
  • docs/zh/dev/ir/09-multi_output_ops.md
  • docs/zh/dev/language/00-python_syntax.md
  • docs/zh/dev/language/01-statements.md
  • docs/zh/dev/language/02-manual_dependencies.md
  • docs/zh/dev/language/05-cache-policy.md
  • docs/zh/dev/passes/00-pass_manager.md
  • docs/zh/dev/passes/01-inline_functions.md
  • docs/zh/dev/passes/05-simplify.md
  • docs/zh/dev/passes/08-outline_graph_scopes.md
  • docs/zh/dev/passes/09-outline_incore_scopes.md
  • docs/zh/dev/passes/10-outline_cluster_scopes.md
  • docs/zh/dev/passes/11-convert_tensor_to_tile_ops.md
  • docs/zh/dev/passes/12-optimize_orch_tensors.md
  • docs/zh/dev/passes/13-lower_composite_ops.md
  • docs/zh/dev/passes/14-flatten_tile_nd_to_2d.md
  • docs/zh/dev/passes/15-block_nz_tensor_views.md
  • docs/zh/dev/passes/16-legalize_tile_cast.md
  • docs/zh/dev/passes/17-auto_tile_matmul_l0.md
  • docs/zh/dev/passes/18-canonicalize_tile_slice.md
  • docs/zh/dev/passes/19-infer_tile_memory_space.md
  • docs/zh/dev/passes/20-insert_mx_scale_addr.md
  • docs/zh/dev/passes/21-resolve_backend_op_layouts.md
  • docs/zh/dev/passes/22-lower_auto_vector_split.md
  • docs/zh/dev/passes/23-expand_mixed_kernel.md
  • docs/zh/dev/passes/24-inject_gm_pipe_buffer.md
  • docs/zh/dev/passes/25-split_vector_kernel.md
  • docs/zh/dev/passes/26-stamp_tfree_split.md
  • docs/zh/dev/passes/27-normalize_return_order.md
  • docs/zh/dev/passes/28-skew_cross_core_pipeline.md
  • docs/zh/dev/passes/29-lower_pipeline_to_slots.md
  • docs/zh/dev/passes/30-lower_pipeline_loops.md
  • docs/zh/dev/passes/31-canonicalize_io_order.md
  • docs/zh/dev/passes/32-materialize_tensor_strides.md
  • docs/zh/dev/passes/33-init_memref.md
  • docs/zh/dev/passes/34-materialize_semantic_aliases.md
  • docs/zh/dev/passes/35-memory_reuse.md
  • docs/zh/dev/passes/36-allocate_memory_addr.md
  • docs/zh/dev/passes/37-fold_no_op_reshape.md
  • docs/zh/dev/passes/38-fuse_create_assemble_to_slice.md
  • docs/zh/dev/passes/39-derive_call_directions.md
  • docs/zh/dev/passes/40-auto_derive_task_dependencies.md
  • docs/zh/dev/passes/41-expand_manual_phase_fence.md
  • docs/zh/dev/passes/42-synthesize_allreduce_signals.md
  • docs/zh/dev/passes/43-materialize_comm_domain_scopes.md
  • docs/zh/dev/passes/44-lower_host_tensor_collectives.md
  • docs/zh/dev/passes/45-materialize_dist_tensor_ctx.md
  • docs/zh/dev/passes/46-legalize_graph_boundary.md
  • docs/zh/dev/passes/47-materialize_runtime_scopes.md
  • docs/zh/dev/passes/48-classify_iter_arg_carry.md
  • docs/zh/dev/passes/49-insert_comm_fence.md
  • docs/zh/dev/passes/50-materialize_valid_shape_symbols.md
  • docs/zh/dev/passes/99-verifier.md
  • docs/zh/dev/passes/index.md
  • docs/zh/reference/pto-isa/index.md
  • docs/zh/user/index.md
  • docs/zh/user/language/00-types.md
  • docs/zh/user/language/01-functions.md
  • docs/zh/user/language/02-control-flow.md
  • docs/zh/user/language/03-memory.md
  • docs/zh/user/language/04-scopes.md
  • docs/zh/user/ops/00-dispatch.md
  • docs/zh/user/ops/01-catalog.md
  • docs/zh/user/precision/00-workflow.md
  • docs/zh/user/precision/01-cases.md
  • docs/zh/user/precision/index.md
  • docs/zh/user/tasks/00-model.md
  • docs/zh/user/tasks/01-scopes.md
  • docs/zh/user/tasks/index.md
  • docs/zh/user/tools/02-memory-map.md
  • docs/zh/user/tutorials/03-mixed-kernel.md
  • include/pypto/ir/core.h
  • include/pypto/ir/kind_traits.h
  • include/pypto/ir/stmt.h
  • include/pypto/ir/transforms/base/functor.h
  • include/pypto/ir/transforms/base/mutator.h
  • include/pypto/ir/transforms/base/visitor.h
  • include/pypto/ir/transforms/ir_property.h
  • include/pypto/ir/transforms/pass_properties.h
  • include/pypto/ir/transforms/passes.h
  • include/pypto/ir/transforms/utils/attrs.h
  • include/pypto/ir/transforms/utils/memref_utils.h
  • include/pypto/ir/transforms/utils/scope_outline_utils.h
  • include/pypto/ir/verifier/verifier.h
  • mkdocs.yml
  • python/bindings/modules/functor.cpp
  • python/bindings/modules/ir.cpp
  • python/bindings/modules/passes.cpp
  • python/pypto/ir/pass_manager.py
  • python/pypto/language/__init__.py
  • python/pypto/language/distributed/op/tensor_ops.py
  • python/pypto/language/dsl_api.py
  • python/pypto/language/parser/ast_parser.py
  • python/pypto/pypto_core/ir.pyi
  • python/pypto/pypto_core/passes.pyi
  • src/ir/builder.cpp
  • src/ir/op/tile_ops/cross_core.cpp
  • src/ir/op/tile_ops/elementwise.cpp
  • src/ir/serialization/serializer.cpp
  • src/ir/serialization/type_deserializers.cpp
  • src/ir/transforms/block_nz_tensor_views_pass.cpp
  • src/ir/transforms/canonicalize_tile_slice_pass.cpp
  • src/ir/transforms/convert_to_ssa_pass.cpp
  • src/ir/transforms/flatten_call_expr_pass.cpp
  • src/ir/transforms/inline_functions_pass.cpp
  • src/ir/transforms/ir_property.cpp
  • src/ir/transforms/memory_reuse_pass.cpp
  • src/ir/transforms/mutator.cpp
  • src/ir/transforms/outline_graph_scopes_pass.cpp
  • src/ir/transforms/python_printer.cpp
  • src/ir/transforms/skew_cross_core_pipeline_pass.cpp
  • src/ir/transforms/structural_equal.cpp
  • src/ir/transforms/structural_hash.cpp
  • src/ir/transforms/utils/dead_code_elimination.cpp
  • src/ir/transforms/utils/scope_outline_utils.cpp
  • src/ir/transforms/utils/transform_utils.cpp
  • src/ir/transforms/visitor.cpp
  • src/ir/verifier/property_verifier_registry.cpp
  • tests/st/runtime/framework_and_models/test_graph_execution.py
  • tests/ut/ir/statements/test_graph_scope_stmt.py
  • tests/ut/ir/transforms/test_materialize_comm_domain_scopes.py
  • tests/ut/ir/transforms/test_materialize_tensor_strides_pass.py
  • tests/ut/ir/transforms/test_memory_reuse.py
  • tests/ut/ir/transforms/test_normalize_return_order.py
  • tests/ut/ir/transforms/test_outline_graph_scopes.py
  • tests/ut/ir/transforms/test_pass_manager.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c812299a3d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ir/transforms/outline_graph_scopes_pass.cpp Outdated
Comment thread src/ir/transforms/outline_graph_scopes_pass.cpp
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 2, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T09:03:02.031990Z c812299 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@lyfne123
lyfne123 force-pushed the feat/hbg-p3-graph-scope-form branch 6 times, most recently from e0101ce to 01b6758 Compare September 3, 2026 10:50
Marking a decoder layer as a recordable graph has so far required lifting it
into its own `@pl.jit.graph` function. That is the wrong shape when the region
is a slice of a larger orchestration body: the lift is mechanical, it invents a
name and a parameter list the author did not want, and it turns a one-line
annotation into an unreviewable diff.

`with pl.graph("name"):` marks the region where it is written. A new
`OutlineGraphScopes` pass lifts it back out into a `FunctionType::Graph`
function named after the region, running immediately before
`OutlineIncoreScopes` — so the `pl.at` scopes inside it are outlined on exactly
the same terms as those in a hand-written Graph function, and everything
downstream (LegalizeGraphBoundary, the Graph verifier, orchestration codegen)
sees one representation rather than two. Placing the pass there also keeps the
new `GraphScopeStmt` alive across three passes instead of forty, which is what
the RFC identifies as the real cost of a scope-shaped carrier.

Parameter order is the one thing that does not carry over: the outliner appends
in capture order while the decorator form uses the declared signature. The two
boundaries are permutations of each other, and nothing downstream reads a
boundary parameter by position.

The region name is required, unlike every other scope kind's optional
`name_hint`. It becomes the outlined function's name, hence the emitted symbol,
hence the runtime's graph key, so an auto-generated name would silently change a
recorded graph's identity whenever an unrelated region was added earlier in the
file. Two regions asking for the same name are disambiguated rather than merged;
sharing one name would give two topologies one Definition and make the second
call replay the first one's graph.

Four placements are compile errors rather than silent degradation, because the
runtime's answer to each is to fall back to ordinary submits with correct
numbers and no diagnostic — the feature would simply stop paying, invisibly. A
Graph region is rejected when nested in another Graph region, when nested in
`pl.at` / `pl.cluster` / `pl.spmd` (those become one device task, whereas a
Graph region records a topology of them), when written in a device kernel body,
and when given no name.

The outliner resolves its callees. A capture a region only ever hands to an
inner kernel's Out/InOut slot is otherwise left at the seeded In: write evidence
comes from the operator registry, which knows nothing about a GlobalVar callee.
Under-declaring is the silent direction — an In boundary tensor is not a writer,
so it loses its RAW edge, where over-declaring only over-orders. A Graph region
is where this bites, its body being a topology of kernel calls rather than of
registry-backed tile ops. ConvertTensorToTileOps repaired it later by accident;
a tensor-to-tile pass is not where param directions should be decided.

Resolving callees also makes the read collector's Submit walk matter. That
override replaces the base walk instead of extending it, and so dropped
`core_num_` and `predicate_` — both first-class SSA operands, not metadata. It
was harmless only while no outliner passed a program: with no callee to resolve,
no argument was ever skipped and every capture was read through its argument
anyway. Once an Out slot is skipped, a predicate can be a capture's only read,
and `pl.spmd_submit(k, rc, predicate=(rc[0,0] > 0))` — the predicate reading the
same value the launch overwrites — yielded Out where the region does read rc on
the way in. Both are now visited.

ScopeOutliner no longer recomputes a block's tail-use set at every position.
That set is read only by the target-scope paths, so it is now computed at the
positions that are, or contain, a scope of the kind being outlined — found by
one linear presence walk. A region among ordinary statements drops from
quadratic to linear (800 statements: 185ms to 10ms), which is what puts the pass
inside the O(N log N) bound of .claude/rules/pass-complexity.md for programs
that use the feature, rather than only for those that skip the outliner
entirely. The computed value is unchanged, and the win reaches the InCore,
hierarchy and cluster outliners too.

Inserting the pass at pipeline position 8 renumbers pass docs 08-49 to 09-50,
and the pass numbers named in prose across docs/ are re-synced with it.

Implements the P3 scope-form item of RFC hw-native-sys#2399.
@lyfne123
lyfne123 force-pushed the feat/hbg-p3-graph-scope-form branch from 01b6758 to e2a70f1 Compare September 3, 2026 11:37
@Hzfengsy
Hzfengsy merged commit ff021f8 into hw-native-sys:main Sep 4, 2026
36 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

2 participants