Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
1954d03
feat(graph): add qd.graph_parallel/qd.branch concurrent branches in g…
hughperkins Jun 9, 2026
616658c
test(graph): add qd.graph_parallel tests + docs; allow if-static opti…
hughperkins Jun 9, 2026
8d2f374
fix(graph): tag a loop's bound-compute serial task with its branch gr…
hughperkins Jun 9, 2026
d00e736
fix(graph): import contextmanager in misc.py
hughperkins Jun 23, 2026
a95d2b7
docs(graph): drop stream_parallel lowering note from graph_parallel b…
hughperkins Jun 23, 2026
70cb697
refactor(graph): rename qd.graph_parallel -> qd.graph_parallel_contex…
hughperkins Jun 23, 2026
706fd60
docs/graph: drop name= param from qd.graph_parallel; tighten graph_pa…
hughperkins Jun 23, 2026
89f8f26
docs/graph: simplify graph_parallel section (sections not branches, t…
hughperkins Jun 23, 2026
c57e452
refactor(graph): use 'parallel section' for graph_parallel internals …
hughperkins Jun 23, 2026
4fdf6e4
test(graph): use 'parallel section' in graph_parallel tests
hughperkins Jun 23, 2026
c334f8b
test(graph): clearer name for graph_parallel_context body-validation …
hughperkins Jun 23, 2026
6592249
test(graph): construct-based name for graph_parallel-outside-context …
hughperkins Jun 23, 2026
a7988b3
docs/graph: drop CUDA conditional while nodes note from do-while sema…
hughperkins Jun 23, 2026
509adfd
docs/graph: trim intro of graph_parallel_context section
hughperkins Jun 23, 2026
8a110e0
docs/graph: use American spellings (behavior/honored/recognized)
hughperkins Jun 23, 2026
4181741
docs/graph: rename term 'parallel section' -> 'qd.graph_parallel sect…
hughperkins Jun 23, 2026
478ec70
style: apply pre-commit (black, clang-format) formatting
hughperkins Jun 23, 2026
16dfba6
Merge branch 'main' into hp/graph-parallel-main
hughperkins Jun 23, 2026
7b4a0b6
feat(graph): generate qd.graph_parallel sections from a qd.static for…
hughperkins Jun 23, 2026
94a9b5b
refactor(graph): extract graph_parallel transformer + doc/wrap CI fixes
hughperkins Jun 23, 2026
c533755
fix(graph): add qd.graph_parallel{,_context} to test_api expected sur…
hughperkins Jun 23, 2026
9f35526
fix up doc ci flagged issues
hughperkins Jun 24, 2026
0a7cb98
Merge branch 'main' into hp/graph-parallel-main
hughperkins Jun 24, 2026
4c80eb0
address doc CI issues
hughperkins Jun 24, 2026
0ba1929
fix(graph): keep joins between back-to-back qd.graph_parallel_context…
hughperkins Jun 24, 2026
66efb1c
refactor(graph): extract graph_parallel context managers out of lang/…
hughperkins Jun 24, 2026
29a2c3d
style(graph): reflow under-wrapped C++ comments to 120c
hughperkins Jun 25, 2026
b304f57
Merge branch 'main' into hp/graph-parallel-main
hughperkins Jun 27, 2026
41176f5
Merge branch 'main' into hp/graph-parallel-main
hughperkins Jul 15, 2026
8b7f4a9
fix(graph): stamp region id on serial statements so serial-only regio…
hughperkins Jul 15, 2026
0d48f15
fix(graph): include graph_parallel_region_id in the offline cache key
hughperkins Jul 15, 2026
6b1bb90
fix(graph): reject runtime if in a qd.graph_parallel_context() region…
hughperkins Jul 15, 2026
7f43107
fix(graph): reject nested graph constructs inside a qd.graph_parallel…
hughperkins Jul 15, 2026
efebd90
refactor(graph): single-source the graph-region tag fields in the off…
hughperkins Jul 15, 2026
4649c00
refactor(graph): consolidate qd.graph_parallel grammar validation int…
hughperkins Jul 15, 2026
143dd83
test(graph): regression for back-to-back regions in graph_do_while (s…
hughperkins Jul 15, 2026
67f90da
fix(graph): keep per-region joins in the streaming launchers (CUDA + …
hughperkins Jul 15, 2026
8b1b61b
Merge branch 'hp/graph-parallel-main' into hp/graph-region-tag-cleanup
hughperkins Jul 15, 2026
f8742bc
refactor(graph): single-source the fork/join run boundary (next_strea…
hughperkins Jul 15, 2026
cf039e7
test(graph): graph_parallel_context in a checkpoints=True kernel must…
hughperkins Jul 16, 2026
ea87bfc
fix(graph): reject qd.graph_parallel_context() in a checkpoints=True …
hughperkins Jul 16, 2026
60520d2
test(graph): section-checkpoint rejection uses a plain graph kernel
hughperkins Jul 16, 2026
c1d2ec3
Merge branch 'hp/graph-parallel-main' into hp/graph-region-tag-cleanup
hughperkins Jul 16, 2026
c52a9ac
refactor(ir): single-source graph-region cache-key fields via GraphRe…
hughperkins Jul 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions docs/source/user_guide/graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,71 @@ while status.yielded:

The restriction is by design: each top-level statement inside a checkpoint becomes its own GPU task / graph node, so silently wrapping bare statements would hide a sequence of N field writes ballooning into N kernel launches.

## `qd.graph_parallel` sections with `qd.graph_parallel_context` *(experimental)*

A `with qd.graph_parallel_context():` region lets you declare independent stages so the graph runs them concurrently.

`qd.graph_parallel_context` is honored by the graph builder so it composes with `graph=True` and `graph_do_while`.

```python
@qd.kernel(graph=True)
def step(...):
while qd.graph_do_while(ncond):
assemble_shared(...) # serial: feeds both `qd.graph_parallel` sections

with qd.graph_parallel_context(): # fork: `qd.graph_parallel` sections run concurrently
with qd.graph_parallel(): # point-triangle contacts
pt_assemble(...)
pt_hessian(...)
with qd.graph_parallel(): # edge-edge contacts (independent of pt)
ee_assemble(...)
ee_hessian(...)
# join: everything below waits for BOTH `qd.graph_parallel` sections to finish
merge_hessians(...)
precondition(...)
```

### Semantics

- **Fork / join.** Every `qd.graph_parallel` section in the region forks from the work that precedes the region. All `qd.graph_parallel` sections must finish before any work *after* the region begins (the join).
- **`qd.graph_parallel` sections are independent - you guarantee it.** Calls *within* a `qd.graph_parallel` section keep their program order, but calls in *different* `qd.graph_parallel` sections have no ordering. The `qd.graph_parallel` sections must be data-race free with respect to one another: no `qd.graph_parallel` section may read what another writes, and no two `qd.graph_parallel` sections may write the same memory. Quadrants does not check this; getting it wrong gives nondeterministic results.

### Generating `qd.graph_parallel` sections from a compile-time sequence

`qd.graph_parallel` sections do not have to be written out one by one. A `for ... in qd.static(...)` loop is unrolled at compile time, so each iteration that contains a `with qd.graph_parallel():` becomes its own section - handy for forking one section per element of a static list (e.g. per contact type):

(Note: See [compound_types.md](compound_types.md) for qd.data_oriented description)
```python
@qd.data_oriented
class Solver:
def __init__(self):
self.funcs = [self._assemble_pt, self._assemble_ee] # static list of @qd.func members

@qd.kernel(graph=True)
def step(self):
with qd.graph_parallel_context():
for i in qd.static(range(len(self.funcs))): # unrolls to one section per func
with qd.graph_parallel():
self.funcs[i]()
```

The loop **must** be a `qd.static(...)` loop (its trip count is known at compile time). A plain runtime `for i in range(n):` is rejected - a runtime loop cannot be unrolled into independent sections.

### Restrictions (enforced at kernel compile time)

- `qd.graph_parallel_context` may contain only `with qd.graph_parallel():` blocks, optionally wrapped in `if qd.static(...)` (so an optional `qd.graph_parallel` section can be compiled in or out - e.g. enabling edge-edge contacts only when a feature flag is set) or `for ... in qd.static(...)` loops (generate one `qd.graph_parallel` section per element of a compile-time sequence).
- `qd.graph_parallel()` may appear only directly inside a `qd.graph_parallel_context()`.
- `qd.graph_parallel_context` cannot be nested, and a `qd.graph_parallel` section body must be straight-line task work - no `qd.graph_do_while`, `qd.checkpoint`, or nested `qd.graph_parallel_context` inside a `qd.graph_parallel` section (a `qd.graph_parallel_context` may, however, sit inside a `qd.graph_do_while` body, as shown above).

### Backend behavior

| backend | scheduling |
| --- | --- |
| CUDA | `qd.graph_parallel` sections run **concurrently** |
| AMDGPU / CPU / Vulkan / Metal | `qd.graph_parallel` sections run **serially** |

Because `qd.graph_parallel` sections are independent by construction, running them serially produces identical results - only the scheduling differs.

## Backend support

`graph=True` and `graph_do_while` run on every backend. They are *hardware accelerated* on CUDA (via [CUDA graphs](https://docs.nvidia.com/cuda/cuda-programming-guide/04-special-topics/cuda-graphs.html)) and AMDGPU (via [HIP graphs](https://rocm.docs.amd.com/projects/HIP/en/latest/how-to/hip_runtime_api/hipgraph.html)); `graph_do_while` additionally requires [CUDA SM 9.0+](https://developer.nvidia.com/cuda/gpus) for its hardware-accelerated path. On other backends, `graph=True` is silently ignored and the kernel runs via the normal launch path, and `graph_do_while` falls back to a host-side do-while loop. `qd.checkpoint` gating runs entirely on the device on every GPU backend.
Expand All @@ -270,6 +335,7 @@ The restriction is by design: each top-level statement inside a checkpoint becom
| `graph=True` | hardware accelerated | hardware accelerated | hardware accelerated | runs (no acceleration) | runs (no acceleration) | runs (no acceleration) |
| `qd.graph_do_while` | hardware accelerated | host fallback | host fallback | host fallback | host fallback | host fallback |
| `qd.checkpoint` | GPU-side | GPU-side | GPU-side | GPU-side | GPU-side | host-side |
| `qd.graph_parallel_context` / `qd.graph_parallel` (sections) | concurrent | concurrent | runs serially | runs serially | runs serially | runs serially |

AMDGPU `graph_do_while` falls back to the host-side loop because HIP does not currently expose conditional / while graph nodes (as of [ROCm](https://www.amd.com/en/products/software/rocm.html) 7.2).

Expand Down
2 changes: 2 additions & 0 deletions docs/source/user_guide/streams.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ combine() # runs after compute_ab() returns — a[] and b[] are ready

Consecutive `with qd.stream_parallel():` blocks run concurrently. Multiple for loops within a single block share a stream and run serially on it. All streams are synchronized before the kernel returns.

> **For `graph=True` kernels**, use [`qd.graph_parallel_context` / `qd.graph_parallel`](graph.md#qdgraph_parallel-sections-with-qdgraph_parallel_context-experimental) instead - `stream_parallel` is not compatible with graphs (see [Limitations](#limitations)). `qd.graph_parallel_context` expresses the same "run these independent sequences concurrently" idea but is honored by the graph builder.

### Restrictions

- All top-level statements in a kernel must be either all `stream_parallel` blocks or all regular statements. Mixing the two at the top level is a compile-time error.
Expand Down
12 changes: 11 additions & 1 deletion python/quadrants/lang/ast/ast_transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
from quadrants.lang.ast.ast_transformers.function_def_transformer import (
FunctionDefTransformer,
)
from quadrants.lang.ast.ast_transformers.graph_parallel_transformer import (
GraphParallelTransformer,
)
from quadrants.lang.exception import (
QuadrantsIndexError,
QuadrantsRuntimeTypeError,
Expand Down Expand Up @@ -1615,9 +1618,16 @@ def build_With(ctx: ASTTransformerFuncContext, node: ast.With) -> None:
if checkpoint_info is not None:
return ASTTransformer._build_checkpoint_with(ctx, node, checkpoint_info)

if GraphParallelTransformer.is_graph_parallel_context_call(item.context_expr):
return GraphParallelTransformer.build_graph_parallel_context_with(ctx, node, build_stmts)

if GraphParallelTransformer.is_parallel_section_call(item.context_expr):
return GraphParallelTransformer.build_parallel_section_with(ctx, node, build_stmts)

if not FunctionDefTransformer._is_stream_parallel_with(node, ctx.global_vars):
raise QuadrantsSyntaxError(
"'with' in Quadrants kernels only supports qd.stream_parallel() or qd.checkpoint()"
"'with' in Quadrants kernels only supports qd.stream_parallel(), qd.checkpoint(), "
"qd.graph_parallel_context(), or qd.graph_parallel()"
)
if not ctx.is_kernel:
raise QuadrantsSyntaxError("qd.stream_parallel() can only be used inside @qd.kernel, not @qd.func")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,38 @@ def _is_loop_config_call(stmt: ast.stmt) -> bool:
return True
return False

@staticmethod
def _is_graph_parallel_context_with(stmt: ast.stmt) -> bool:
"""Syntactic check matching GraphParallelTransformer.is_graph_parallel_context_call: a
``with qd.graph_parallel_context():`` fork/join region."""
if not isinstance(stmt, ast.With) or len(stmt.items) != 1:
return False
ctx_expr = stmt.items[0].context_expr
if not isinstance(ctx_expr, ast.Call):
return False
func = ctx_expr.func
if isinstance(func, ast.Attribute) and func.attr == "graph_parallel_context":
return True
if isinstance(func, ast.Name) and func.id == "graph_parallel_context":
return True
return False

@staticmethod
def _is_parallel_section_with(stmt: ast.stmt) -> bool:
"""Syntactic check matching GraphParallelTransformer.is_parallel_section_call: a ``with qd.graph_parallel(...):``
section of a ``qd.graph_parallel_context()`` region."""
if not isinstance(stmt, ast.With) or len(stmt.items) != 1:
return False
ctx_expr = stmt.items[0].context_expr
if not isinstance(ctx_expr, ast.Call):
return False
func = ctx_expr.func
if isinstance(func, ast.Attribute) and func.attr == "graph_parallel":
return True
if isinstance(func, ast.Name) and func.id == "graph_parallel":
return True
return False

@staticmethod
def _validate_graph_do_while_structure(body: list[ast.stmt]) -> None:
"""If a kernel uses qd.graph_do_while() anywhere, enforce the structural rules that remain after
Expand Down Expand Up @@ -659,6 +691,27 @@ def _validate_graph_do_while_stmt_list(stmts: list[ast.stmt], is_kernel_top: boo
# `CheckpointTransformer.build_checkpoint_with`.
FunctionDefTransformer._validate_graph_do_while_stmt_list(stmt.body, is_kernel_top=is_kernel_top)
continue
if FunctionDefTransformer._is_graph_parallel_context_with(stmt):
# A `with qd.graph_parallel_context()` region groups concurrent `with qd.graph_parallel()`
# sections; it is a legal sibling of for-loops / checkpoints. Its body must be
# `qd.graph_parallel` section blocks (optionally under `if qd.static(...)`); the full check
# is in GraphParallelTransformer.build_graph_parallel_context_with. Each `qd.graph_parallel` section's
# body is task territory, validated here with the in-loop rules. Descend through `if` members
# so `qd.graph_parallel` sections inside an optional `if qd.static(...)` are reached too.
pending = list(stmt.body)
while pending:
member = pending.pop()
if FunctionDefTransformer._is_parallel_section_with(member):
FunctionDefTransformer._validate_graph_do_while_stmt_list(member.body, is_kernel_top=False)
elif isinstance(member, ast.If):
pending.extend(member.body)
pending.extend(member.orelse)
elif isinstance(member, ast.For):
# `for ... in qd.static(...)` generates sections; descend so each unrolled section
# body is still validated with the in-loop rules (a runtime for here is rejected
# earlier by GraphParallelTransformer.build_graph_parallel_context_with).
pending.extend(member.body)
continue
where = "the kernel body" if is_kernel_top else "a qd.graph_do_while() body"
raise QuadrantsSyntaxError(
f"When a kernel uses qd.graph_do_while(), {where} may not contain a {type(stmt).__name__} "
Expand Down
Loading
Loading