Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 7 additions & 1 deletion docs/en/dev/ir/05-operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,13 @@ with ib.function("tile_computation") as f:
| `system.task_invalid` | Sentinel `PTO2TaskId::invalid()` — "no producer" seed for a TaskId carry | None |
| `system.task_is_valid` | Test whether a `TASK_ID` value is a valid (non-sentinel) handle | None; sole positional arg is the TaskId Var |

`system.syncall` has two modes. The **hard** form (`mode="hard"`, default) emits an FFTS barrier that waits for **all** physical cores of the selected `core_type`; the kernel must be launched at full occupancy (one block per physical core), or the barrier deadlocks (AICore error 507018). The **soft** form (`mode="soft"`) polls a shared GM workspace and so works at **partial** occupancy. Soft mode carries operands `[gm_workspace, scratch, used_cores]`: `gm_workspace` is a shared, zero-initialized GM `INT32` tensor with `used_cores * 8` slots (pass it as a kernel parameter so all blocks share one buffer); `scratch` is a compiler-synthesized local staging tile; `used_cores` is the participant count. Soft mode currently supports `core_type="aiv_only"` only.
`system.syncall` has two modes. The **hard** form (`mode="hard"`, default) emits an FFTS barrier that waits for **all** physical cores of the selected `core_type`; the kernel must be launched at full occupancy (one block per physical core), or the barrier deadlocks (AICore error 507018). The **soft** form (`mode="soft"`) polls a shared GM workspace and so works at **partial** occupancy. `gm_workspace` is a shared, zero-initialized GM `INT32` tensor with `used_cores * 8` slots (pass it as a kernel parameter so all blocks share one buffer); the scratch tile(s) are compiler-synthesized local staging buffers; `used_cores` is the participant count. Soft mode is supported for every `core_type`, with operands that vary by participant set:

- `aiv_only`: `[gm_workspace, ub_scratch, used_cores]` — one UB (Vec) staging tile.
- `aic_only`: `[gm_workspace, l1_scratch, used_cores]` — one flat L1 (Mat, `slayout=none_box`) staging tile.
- `mix`: `[gm_workspace, ub_scratch, l1_scratch, used_cores]` — both a UB and a flat L1 tile. The barrier rendezvouses AIC + AIV cores, so `used_cores` is the *total* participant count (AIC blocks + AIV subblocks). The op is duplicated onto both the cube and vector lanes; each lane uses its own tile (the other is dead), matching pto-isa's soft-mix lowering.

The flat L1 staging tile is created via `pl.tile.create(..., target_memory=pl.Mem.Mat, flat_layout=True)`, which keeps the contiguous `slayout=none_box` layout (a normal boxed NZ Mat tile would mis-place the 8-int32 counter slots).

The unified `mode=` keyword API (`mode="hard"` / `mode="soft"`) is the **DSL** surface (`pl.system.syncall`). The Python IR helpers under `pypto.ir.op.system` are split instead: `syncall(core_type=...)` builds the hard form and `syncall_soft(core_type, args)` builds the soft form.

Expand Down
8 changes: 7 additions & 1 deletion docs/zh-cn/dev/ir/05-operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,13 @@ with ib.function("tile_computation") as f:
| `system.task_invalid` | `PTO2TaskId::invalid()` 哨兵——TaskId carry 的 "暂无 producer" 种子 | 无 |
| `system.task_is_valid` | 测试某个 `TASK_ID` 值是否为有效(非哨兵)handle | 无;唯一位置参数是 TaskId Var |

`system.syncall` 有两种 mode。**hard** 形态(`mode="hard"`,默认)下沉为 FFTS 屏障,等待所选 `core_type` 的**全部**物理核到达;kernel 必须以满占用方式启动(每个物理核一个 block),否则屏障死锁(AICore 错误 507018)。**soft** 形态(`mode="soft"`)轮询一段共享 GM workspace,因此可在**部分**占用下工作。soft 形态带 operand `[gm_workspace, scratch, used_cores]`:`gm_workspace` 是共享、清零的 GM `INT32` tensor,含 `used_cores * 8` 个 slot(请作为 kernel 参数传入,使所有 block 共享同一缓冲);`scratch` 是编译器合成的本地暂存 tile;`used_cores` 是参与核数。soft 形态目前仅支持 `core_type="aiv_only"`。
`system.syncall` 有两种 mode。**hard** 形态(`mode="hard"`,默认)下沉为 FFTS 屏障,等待所选 `core_type` 的**全部**物理核到达;kernel 必须以满占用方式启动(每个物理核一个 block),否则屏障死锁(AICore 错误 507018)。**soft** 形态(`mode="soft"`)轮询一段共享 GM workspace,因此可在**部分**占用下工作。`gm_workspace` 是共享、清零的 GM `INT32` tensor,含 `used_cores * 8` 个 slot(请作为 kernel 参数传入,使所有 block 共享同一缓冲);暂存 tile 由编译器合成;`used_cores` 是参与核数。soft 形态对每种 `core_type` 都支持,operand 随参与核集合而不同:

- `aiv_only`:`[gm_workspace, ub_scratch, used_cores]` —— 一个 UB(Vec)暂存 tile。
- `aic_only`:`[gm_workspace, l1_scratch, used_cores]` —— 一个扁平 L1(Mat,`slayout=none_box`)暂存 tile。
- `mix`:`[gm_workspace, ub_scratch, l1_scratch, used_cores]` —— UB 与扁平 L1 各一个。该屏障汇合 AIC + AIV 核,故 `used_cores` 是**总**参与数(AIC block 数 + AIV subblock 数)。该 op 会被复制到 cube 与 vector 两条流上,每条流各用自己的 tile(另一个在该流上是死代码),与 pto-isa 的 soft-mix 下沉一致。

扁平 L1 暂存 tile 通过 `pl.tile.create(..., target_memory=pl.Mem.Mat, flat_layout=True)` 创建,保持连续的 `slayout=none_box` 布局(普通的 boxed NZ Mat tile 会错位 8 个 int32 计数槽)。

统一的 `mode=` 关键字 API(`mode="hard"` / `mode="soft"`)是 **DSL** 层接口(`pl.system.syncall`)。`pypto.ir.op.system` 下的 Python IR 辅助函数则是拆开的:`syncall(core_type=...)` 构造 hard 形态,`syncall_soft(core_type, args)` 构造 soft 形态。

Expand Down
17 changes: 12 additions & 5 deletions python/pypto/ir/op/system_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,8 @@ def syncall_soft(core_type: str, args: list[Expr], *, span: Span | None = None)
works at partial occupancy. ``args`` is the positional operand list, already
assembled by the DSL layer:

- aiv_only / aic_only: ``[gm_workspace, scratch_tile, used_cores]``
- aiv_only: ``[gm_workspace, ub_scratch, used_cores]``
- aic_only: ``[gm_workspace, l1_scratch, used_cores]``
- mix: ``[gm_workspace, ub_scratch, l1_scratch, used_cores]``

Args:
Expand All @@ -182,10 +183,16 @@ def syncall_soft(core_type: str, args: list[Expr], *, span: Span | None = None)
Returns:
Call expression for the soft-mode system.syncall.
"""
if core_type != "aiv_only":
# Soft form currently only has a validated lowering for aiv_only. Gate the
# IR helper too so direct IR callers cannot build an unsupported barrier.
raise ValueError(f"soft syncall currently supports only core_type='aiv_only', got {core_type!r}")
if core_type not in _SYNCALL_CORE_TYPES:
raise ValueError(f"soft syncall core_type must be one of {_SYNCALL_CORE_TYPES}, got {core_type!r}")
# aiv_only/aic_only carry one scratch tile (3 operands); mix carries both a UB
# and a flat L1 scratch (4 operands). Gate the arity here so direct IR callers
# cannot build a malformed barrier.
expected = 4 if core_type == "mix" else 3
if len(args) != expected:
raise ValueError(
f"soft syncall core_type={core_type!r} requires {expected} operands, got {len(args)}"
)
actual_span = _get_span_or_capture(span, frame_offset=1)
return _ir_core.create_op_call(
"system.syncall", args, {"core_type": core_type, "mode": "soft"}, actual_span
Expand Down
12 changes: 12 additions & 0 deletions python/pypto/ir/op/tile_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ def create(
target_memory: MemorySpace = MemorySpace.Vec,
transpose: bool | None = None,
span: Span | None = None,
*,
flat_layout: bool | None = None,
) -> Call:
"""Create a tile from a shape.

Expand All @@ -133,6 +135,14 @@ def create(
is omitted from the op kwargs, so ordinary ``tile.create`` output is
unchanged; only forwarded to the op when explicitly set.
span: Optional source span for debugging (auto-captured if not provided)
flat_layout: Keyword-only. When True, allocate a flat (non-fractal,
slayout=none_box) L1/cbuf tile — a contiguous byte-staging buffer
rather than the boxed NZ layout Mat tiles normally carry. Requires
``target_memory=Mat`` and is mutually exclusive with ``transpose``.
Used for the mix/aic_only soft ``system.syncall`` L1 scratch, whose
counter slots must be contiguous. Default ``None`` keeps the
canonical layout. Kept keyword-only so it does not shift ``span``'s
positional slot for existing callers.

Returns:
Call expression that returns a TileType with the created tile
Expand All @@ -142,6 +152,8 @@ def create(
kwargs: dict[str, Any] = {"dtype": dtype, "target_memory": target_memory}
if transpose is not None:
kwargs["transpose"] = transpose
if flat_layout is not None:
kwargs["flat_layout"] = flat_layout
return _ir_core.create_op_call("tile.create", [shape_tuple], kwargs, actual_span)


Expand Down
70 changes: 53 additions & 17 deletions python/pypto/language/op/system_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,17 @@
]


_SYNCALL_SOFT_CORE_TYPES = ("aiv_only", "aic_only", "mix")


def syncall(
*,
core_type: str = "mix",
mode: str = "hard",
gm_workspace: Tensor | None = None,
used_cores: int = 0,
scratch: Tile | None = None,
scratch_l1: Tile | None = None,
span: Span | None = None,
) -> Call:
"""Cross-core all-participant barrier (``pto::SYNCALL``).
Expand All @@ -77,20 +81,28 @@ def syncall(
deadlocks). See :func:`pypto.ir.op.system_ops.syncall`.
- ``mode="soft"``: GM-polling barrier that works at partial occupancy.
Each participant bumps a per-core counter in a shared GM workspace and
polls until all ``used_cores`` participants arrive.
polls until all ``used_cores`` participants arrive. Supported for every
``core_type`` ("aiv_only", "aic_only", "mix").

Soft-mode arguments:

Args:
core_type: Participant set, one of "aiv_only", "aic_only", or "mix".
For "mix", ``used_cores`` is the *total* AIC + AIV participant count.
mode: "hard" or "soft".
gm_workspace: Soft mode only. A shared, zero-initialized GM ``INT32``
tensor with at least ``used_cores * 8`` elements, visible to every
participating block (pass it as a kernel parameter so all SPMD
blocks share one buffer). The compiler synthesizes the local
UB/L1 staging tile automatically.
UB/L1 staging tile(s) automatically.
used_cores: Soft mode only. Number of participating cores (a positive
compile-time int).
scratch: Compiler-internal. The local staging tile threaded back by the
printer on reparse (UB/Vec tile for "aiv_only" and "mix"; flat
L1/Mat tile for "aic_only"). Leave ``None`` in user code.
scratch_l1: Compiler-internal. The flat L1/Mat staging tile for the
"mix" form, threaded back by the printer on reparse. Leave ``None``
in user code.
span: Optional source span for debugging (auto-captured if not provided).

Returns:
Expand All @@ -100,33 +112,57 @@ def syncall(
# Reject soft-only kwargs so a typo like syncall(gm_workspace=ws) does not
# silently fall back to the full-occupancy hard barrier (the deadlock path
# the soft form exists to avoid).
if gm_workspace is not None or scratch is not None or used_cores:
if gm_workspace is not None or scratch is not None or scratch_l1 is not None or used_cores:
raise ValueError(
"syncall(mode='hard') takes no gm_workspace/scratch/used_cores; "
"syncall(mode='hard') takes no gm_workspace/scratch/scratch_l1/used_cores; "
"pass mode='soft' to use the GM-polling barrier"
)
return _ir_ops.syncall(core_type=core_type, span=span)
if mode != "soft":
raise ValueError(f"syncall mode must be 'hard' or 'soft', got {mode!r}")
# Soft form currently supports only aiv_only. aic_only needs a flat L1 (cbuf)
# staging buffer (a fractal-laid-out Mat tile mis-places the counter slots),
# and mix needs both UB + L1 scratch across a mixed kernel — both follow-ups.
if core_type != "aiv_only":
raise ValueError(f"soft syncall currently supports only core_type='aiv_only', got {core_type!r}")
if core_type not in _SYNCALL_SOFT_CORE_TYPES:
raise ValueError(
f"soft syncall core_type must be one of {_SYNCALL_SOFT_CORE_TYPES}, got {core_type!r}"
)
if gm_workspace is None:
raise ValueError("soft syncall requires gm_workspace (a shared, zero-initialized GM INT32 tensor)")
if not isinstance(used_cores, int) or used_cores <= 0:
raise ValueError(f"soft syncall requires a positive compile-time used_cores, got {used_cores!r}")

actual_span = _get_span_or_capture(span, frame_offset=1)
if scratch is None:
# User path: synthesize the local UB staging tile. (On reparse the printer
# threads the existing scratch back via scratch=, so we reuse it instead.)
from . import tile_ops as _tile # noqa: PLC0415 # deferred: tile_ops imports system_ops (cycle)

slots = used_cores * _SYNCALL_SOFT_SLOT_INT32
scratch = _tile.create([1, slots], DataType.INT32, target_memory=MemorySpace.Vec)
args = [gm_workspace.unwrap(), scratch.unwrap(), ConstInt(used_cores, DataType.INT32, actual_span)]
# Deferred import: tile_ops imports system_ops (cycle).
from . import tile_ops as _tile # noqa: PLC0415

def _ub_scratch(existing: Tile | None) -> Tile:
# UB (Vec) staging tile. The AIV barrier bulk-reads every participant's
# slot into it, so it needs used_cores * 8 int32 (flat by default).
if existing is not None:
return existing
return _tile.create(
[1, used_cores * _SYNCALL_SOFT_SLOT_INT32], DataType.INT32, target_memory=MemorySpace.Vec
)

def _l1_scratch(existing: Tile | None) -> Tile:
# Flat L1 (Mat/cbuf) staging tile. The cube path only stages its own
# single counter line via create_cbuf_matrix, so 8 int32 suffice; it must
# be flat (slayout=none_box) or the counter slot is mis-placed.
if existing is not None:
return existing
return _tile.create(
[1, _SYNCALL_SOFT_SLOT_INT32], DataType.INT32, target_memory=MemorySpace.Mat, flat_layout=True
)

used_const = ConstInt(used_cores, DataType.INT32, actual_span)
if core_type == "aiv_only":
scratch = _ub_scratch(scratch)
args = [gm_workspace.unwrap(), scratch.unwrap(), used_const]
elif core_type == "aic_only":
scratch = _l1_scratch(scratch)
args = [gm_workspace.unwrap(), scratch.unwrap(), used_const]
else: # mix: both a UB and a flat L1 staging tile
scratch = _ub_scratch(scratch)
scratch_l1 = _l1_scratch(scratch_l1)
args = [gm_workspace.unwrap(), scratch.unwrap(), scratch_l1.unwrap(), used_const]
return _ir_ops.syncall_soft(core_type, args, span=actual_span)


Expand Down
8 changes: 8 additions & 0 deletions python/pypto/language/op/tile_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ def create(
dtype: DataType,
target_memory: MemorySpace = MemorySpace.Vec,
transpose: bool | None = None,
*,
flat_layout: bool | None = None,
) -> Tile:
"""Create a tile from a shape.

Expand All @@ -313,6 +315,11 @@ def create(
transpose: When True, allocate the transposed Mat (ZN) fractal layout for a
matmul ``b_trans`` B-operand (the layout a DN-source ``gather_row`` fills).
Default ``None`` keeps the canonical layout and is omitted from the op.
flat_layout: Keyword-only. When True, allocate a flat (non-fractal,
slayout=none_box) L1/cbuf tile — a contiguous staging buffer rather
than the boxed NZ layout Mat tiles normally carry. Requires
``target_memory=Mat`` and is mutually exclusive with ``transpose``.
Default ``None`` keeps the canonical layout.

Returns:
Tile wrapping the create operation
Expand All @@ -324,6 +331,7 @@ def create(
dtype,
target_memory,
transpose,
flat_layout=flat_layout,
)
return Tile(expr=call_expr)

Expand Down
Loading
Loading