Compile and Runtime Workflow¶
What usually happens when you run python <kernel>.py -p <platform>.
Most examples and model harnesses use golden.run for @pl.program kernels
or golden.run_jit for module-level @pl.jit kernels. A few specialized
smoke, artifact-regeneration, and external-runtime drivers call PyPTO's
compile/runtime APIs directly; their __main__ blocks are the authority for
what a command actually validates.
CLI shape¶
A typical model __main__ block parses three flags and dispatches into
the harness:
parser.add_argument("-p", "--platform", choices=["a2a3", "a2a3sim", "a5", "a5sim"])
parser.add_argument("-d", "--device", type=int, default=0)
parser.add_argument("--enable-l2-swimlane", action="store_true")
args = parser.parse_args()
result = run(
program=build_qwen3_decode_program(...), # @pl.program class
specs=build_tensor_specs(...), # ordered TensorSpec / ScalarSpec list
golden_fn=golden_qwen3_decode, # PyTorch reference
compile_cfg=dict(dump_passes=True),
runtime_cfg=dict(platform=args.platform, device_id=args.device,
enable_l2_swimlane=args.enable_l2_swimlane),
rtol=3e-3, atol=3e-3,
)
A kernel written as a module-level @pl.jit function calls run_jit
instead, passing fn=<jit_function> in place of program=. Both entry points
share tensor specs, golden computation, runtime dispatch, and validation, but
their compile_cfg fields and defaults differ; see
Compile configuration.
| Flag | Purpose |
|---|---|
-p / --platform |
Target backend. a2a3 is Ascend 910B/C; a5 is Ascend 950 — both run on real NPU. a2a3sim / a5sim are the matching simulators. |
-d / --device |
Device ID for multi-card hosts. |
--enable-l2-swimlane |
Forwarded to the runtime; collects per-task L2 perf records into the build_output (see Runtime DFX flags). |
a2a3* maps to BackendType.Ascend910B; a5* maps to
BackendType.Ascend950.
Multi-card kernels in CI¶
Most kernels take a single -d <id>. A kernel that needs several NPUs
(e.g. an EP/TP program parsing -d as a comma-separated list) declares its
card count with a marker comment near the top of the file:
The real-NPU CI job greps for # ci: devices=N; when N > 1 it borrows
that many cards from the host device queue with
task-submit --device "$DEVICE_ID" --device-num N. $DEVICE_ID is auto
(borrow any free cards) or a fixed id set by the CI backend, and the lent
set comes back as $TASK_DEVICE, passed straight to -d. Files without the
marker default to one card. Runs use each program's default world size,
commonly EP2 for distributed DeepSeek entries. The current workflow contains
only a commented EP4 command example; it does not provide active EP4 or EP8
per-file coverage. See the a2a3 job in
.github/workflows/ci.yml.
Multi-card kernels use HCCL, which silent-crashes inside docker. For this
reason the real-NPU job runs on the host (no container). The shared
setup-ci-job action writes an activation script that enters the Python
environment and sources CANN's set_env.sh; each task-submit child sources
that script. The workflow also preserves its PTO2_RING_* settings into the
child. Running a multi-card kernel locally needs the same kind of real-device
shell, e.g.
python models/deepseek_v4_flash_mtp/moe.py -p a2a3 --ep 2 -d 0,1.
Phases inside the Golden Harness¶
The harness prints [RUN] <stage> ... / [RUN] <stage> done (Xs) around
each phase, so the console log is the authoritative trace of what ran:
1. Compile (pypto)¶
Driven by the pypto repo. run calls
pypto.ir.compile(program, backend_type=..., **compile_cfg) directly.
run_jit builds a pypto.runtime.RunConfig from compile_cfg and calls
fn.compile(..., config=...), which specializes the JIT function before
entering the same IR compiler. Both paths run a pass pipeline followed by
a codegen pipeline and normally write
build_output/<ProgramName>_<timestamp>/.
1a. Pass pipeline¶
PassManager.get_strategy(strategy).run_passes(program, ...) runs an
ordered sequence of passes that progressively rewrites the IR. The exact
pass list changes often — consult the pypto repo for the current pipeline,
and look at passes_dump/ when dump_passes is enabled. Direct run
compilation inherits ir.compile's enabled default; run_jit inherits
RunConfig's disabled default.
The end state, regardless of which passes ran, is the same:
- exactly one orchestration function (
FunctionType.Orchestration), - plus one InCore function per outlined
pl.at/pl.spmdregion.
A pl.at region that mixes cube and vector ops is split into two
InCore functions during outlining: one cube-only kernel (matmul,
matmul_acc, …) and one vector-only kernel (cast, add, row_sum, …). The
orchestration function calls them in dependency order.
The InCore / Orchestration boundary the frontend left implicit becomes explicit at this stage.
1b. Codegen pipeline¶
pypto.backend.pto_backend.generate(...) walks the transformed program
and emits files in three streams:
- InCore kernels →
.pto→ C++ wrapper. Each kernel function (or group thereof) goes throughPTOCodegento produce an MLIR text file (.pto) underptoas/. Thenptoas(the external assembler/optimizer toolchain) compiles each.ptoto a C++ kernel wrapper underkernels/aic/(cube) orkernels/aiv/(vector). The ptoas invocations run in a thread pool since each is an independent subprocess.skip_ptoas=Truekeeps the raw.ptofiles and skips the C++ wrapper step (useful for inspecting pure MLIR output or for isolating whether a regression came from pypto's IR→MLIR or from ptoas). - Orchestration → C++.
generate_orchestrationemits oneorchestration/<orch_name>.cppthat drives the kernels through the PTO2 runtime API (task graph build, scheduling, dependencies). - Config →
kernel_config.py. Records each kernel's name, runtime ID, and core type (cube / vector) for the runtime to load.
When PTOAS_ROOT is set, PyPTO searches only
$PTOAS_ROOT/ptoas and then $PTOAS_ROOT/bin/ptoas; it deliberately does
not fall back to a potentially mismatched binary on PATH. When
PTOAS_ROOT is unset, PyPTO searches PATH.
Output directory layout¶
build_output/<ProgramName>_<ts>/
├── passes_dump/ # IR after each pass, when dump_passes is enabled
├── ptoas/ # raw .pto MLIR + ptoas intermediates
├── kernels/
│ ├── aic/ # cube kernel C++ wrappers from ptoas
│ └── aiv/ # vector kernel C++ wrappers from ptoas
├── orchestration/ # generated AICPU orchestration C++ (compiled into .so)
├── kernel_config.py
├── report/ # memory allocation + scheduling reports
├── data/ # populated by later phases (in/, out/)
└── dfx_outputs/ # runtime DFX artefacts (any --enable-* flag)
Compile configuration¶
For run, compile_cfg is forwarded to ir.compile. Common fields are:
compile_cfg field |
Purpose |
|---|---|
output_dir |
Override build_output/<name>_<timestamp>/. |
strategy |
Select the optimization strategy. |
dump_passes |
Write pass IR under passes_dump/; defaults to True on this direct compiler path. |
skip_ptoas |
Stop after .pto generation without producing kernel C++ wrappers. |
profiling |
Write compile-stage timing reports under report/. |
verification_level, diagnostic_phase, disabled_diagnostics |
Configure compiler verification and diagnostics. |
distributed_config, analyze_auto_scopes_for_deps, memory_planner |
Configure distributed lowering, AUTO-scope dependency analysis, and memory planning. |
The harness derives backend_type and platform from
runtime_cfg["platform"] unless the direct compile configuration already
supplies them.
For run_jit, compile_cfg must instead contain fields accepted by
pypto.runtime.RunConfig. The JIT layer maps its compile-side fields into
ir.compile:
compile_cfg field |
Compiler mapping |
|---|---|
dump_passes |
Same pass dumps, but the RunConfig default is False. |
save_kernels_dir |
Maps to ir.compile(output_dir=...). |
compile_profiling |
Maps to ir.compile(profiling=...). |
strategy, diagnostic_phase, disabled_diagnostics |
Forwarded to the corresponding compiler fields. |
distributed_config, analyze_auto_scopes_for_deps, memory_planner |
Forwarded when set. |
output_dir, profiling, skip_ptoas, and verification_level are not
RunConfig field names and therefore cannot be copied unchanged from a
run call into run_jit. Unknown fields raise while constructing
RunConfig; unknown direct-compiler fields raise in ir.compile.
To stop after compile without touching the device, see compile_only under
Skipping phases.
2. Generate inputs¶
Each entry of specs is a TensorSpec (named tensor, shape, dtype,
direction) or a ScalarSpec (named scalar, dtype, value); see
golden/spec.py. The list is ordered to match the parameter order of the
top opaque function. For each entry, allocate a torch tensor:
- Pure inputs and inout initial values are filled via
spec.create_tensor()(init_value=Nonecreates zeros; random data requires an explicit factory such astorch.randn). - Pure outputs are zero-initialised.
- Scalars become 0-D tensors carrying the spec value.
See Golden Harness for the complete
TensorSpec initialization contract.
When save_data=True, the input snapshot is written to data/in/<name>.pt
so the same inputs can be replayed later; this is off by default, so no
snapshot is written unless you opt in. If golden_data=<dir> is passed
instead, the harness loads <dir>/in/*.pt rather than generating fresh
data — useful for deterministic regression checks.
3. Compute golden¶
The golden runs before device execution: it depends only on the input
snapshot, not on the runtime, so the reference is ready for validation. With
save_data=True, a later runtime crash still leaves the persisted
data/out/ snapshot.
If golden_fn is provided, run builds a scratch dict with cloned
inputs and zero-init outputs, calls golden_fn(scratch) (which fills the
output entries in place), and — when save_data=True — writes the result
to data/out/<name>.pt.
If golden_data=<dir> is set, the harness loads <dir>/out/*.pt instead
of recomputing — golden_data always wins over golden_fn.
If neither is provided, validation is skipped and the run reports
PASS (validation skipped).
Golden PyTorch operations use 16 intra-op CPU threads by default. Set
PYPTO_GOLDEN_NUM_THREADS to a positive integer to override the repository
default for a run:
4. Runtime (simpler)¶
Driven by the simpler repo (PTO2 runtime). For a single-chip build, the
harness orders the arguments according to specs and calls
pypto.runtime.execute_compiled. For an L3
DistributedCompiledProgram, it instead dispatches the compiled object with a
pypto.runtime.RunConfig; resident-weight L3 programs use the prepared-worker
path. Tensors are mutated in place, so outputs land in the same Python tensors
after dispatch.
runtime_cfg is therefore not forwarded verbatim in every case:
log_levelis consumed by the harness to configure PyPTO's runtime logger;- the five DFX fields below are bundled into the runtime's DFX options on the single-chip path;
- remaining single-chip fields are passed to
execute_compiled, which rejects unknown names; - L3 dispatch retains fields supported by
RunConfig.
Runtime DFX flags¶
PyPTO surfaces simpler's five runtime DFX (Design For X) sub-features as
independent toggles on runtime_cfg. They share the same output
directory and can be enabled in any combination. CLI spellings are
entry-specific; the table lists the common spelling when a script exposes it.
| Kwarg | CLI flag | Artefact under dfx_outputs/ |
|---|---|---|
enable_l2_swimlane=True (or a supported level) |
--enable-l2-swimlane [N] |
l2_swimlane_records.json; onboard runs also attempt merged_swimlane_*.json |
enable_dump_args=<N> (int, 0=off) |
--dump-args [N] (bare = 1) |
args_dump/{args_dump.json,args.bin} |
enable_pmu=<N> (int, 0=off) |
--enable-pmu [N] (bare = 2) |
pmu.csv |
enable_dep_gen=True |
--enable-dep-gen |
deps.json |
enable_scope_stats=True |
--enable-scope-stats |
scope_stats/scope_stats.jsonl |
Args-dump level 1 captures only arguments selected with pl.dump_tag or a
dumps= list; level 2 captures every task's tensor payloads and scalar
values. Level 3 captures the same argument metadata without writing tensor
payloads or args.bin.
For an onboard L2 swimlane run, PyPTO first attempts a dependency-graph
capture and then a clean timing capture so the converter can add dependency
arrows without perturbing the timing pass. Open the generated
merged_swimlane_*.json at
ui.perfetto.dev to visualize per-task
execution on each AICPU / AIC / AIV lane and inspect kernel duration,
gaps, and dependency stalls. Simulator runs retain
l2_swimlane_records.json but do not generate the merged trace because the
required task metadata is unavailable.
The raw dependency and scope-stat files can also be rendered offline:
python -m simpler_setup.tools.deps_viewer \
build_output/<name>/dfx_outputs/deps.json --format html --engine sfdp
python -m simpler_setup.tools.scope_stats_plot \
build_output/<name>/dfx_outputs/scope_stats/scope_stats.jsonl
For kernel-internal swimlanes and MindStudio Insight traces, see
In-Core Simulator Profiling.
The repository workflow can reuse an existing build or drive a case
end-to-end. It writes the export root below
build_output/<ProgramName>_<ts>/kernel_insight_all_funcs_<ts>/.
See pypto's docs/en/dev/03-runtime-dfx.md and the simpler reference at
runtime/docs/dfx/{l2-swimlane-profiling,args-dump,pmu-profiling,dep_gen,scope-stats}.md
for full per-flag details. There is no runtime_profiling /
--runtime-profiling compatibility alias in the current Python API; use
enable_l2_swimlane or the entry's --enable-l2-swimlane flag.
4b. Benchmark (opt-in, before validation)¶
With PYPTO_BENCH=1 in the environment, the harness re-dispatches the
compiled program in a timed loop right after the correctness dispatch and
before validation, and prints
[RUN] effective_us (N rounds) min=… median=… mean=… max=…. It is
env-gated only — no model file needs a flag. PYPTO_BENCH_ROUNDS /
PYPTO_BENCH_WARMUP (default 100 / 5) size the loop and PYPTO_BENCH_RAW
dumps the per-dispatch samples. See
performance-tuning.md
for the output format, the multi-card breakdown, and what the number means.
5. Validate¶
golden.validation.validate_golden compares each device output against
the golden using torch.allclose(rtol, atol) by default. Override
per-output with the compare_fn={"out_name": custom_callable} argument.
golden.validation ships three ready-made comparators:
| Comparator | Use case |
|---|---|
topk_pair_compare(vals_name) |
Top-k index outputs whose ordering is implementation-dependent — checks the paired value tensor matches after sort, tolerating legal tie-break swaps. |
ratio_allclose(atol, rtol, max_error_ratio=0.005) |
Quantized kernels where a small outlier fraction may exceed per-point atol + rtol·|expected|. NaN/Inf always fail. |
ratio_reldiff(diff_thd, pct_thd, max_diff_hd=inf) |
cann-recipes-infer-style relative-diff check: per-point rdiff > diff_thd bad-point ratio capped by pct_thd, with optional single-point max_diff_hd cap. |
Both ratio comparators also take valid_rows / valid_axis / zero_tail:
valid_rows=n compares only the leading n entries along valid_axis
(default axis 0), so a packed buffer's inactive token tail cannot dilute the
error ratio, and zero_tail=True additionally fails the check when that
dropped tail is not all zeros. valid_rows=0 compares nothing and passes.
The harness returns RunResult(passed=True) on success. Validation mismatches
and a small set of harness-level setup errors return
RunResult(passed=False, error=...). Compiler errors, golden-function errors,
and ordinary runtime exceptions generally propagate instead of being converted
to RunResult. A CLI should check a returned result and exit nonzero when it is
false; an uncaught compile/runtime exception is already a nonzero failure.
Skipping phases¶
run / run_jit knobs that short-circuit the pipeline:
| Knob | Effect |
|---|---|
compile_only=True |
Stops after the compile phase. Useful in CI smoke tests that just check the program lowers cleanly. |
runtime_dir="<path>" |
Skips compile and reuses an existing build_output/<...> directory. Useful when iterating on golden_fn or validation logic without recompiling. |
golden_data="<path>" |
Loads inputs from <path>/in/ and goldens from <path>/out/ instead of generating them. golden_data overrides golden_fn. Useful for deterministic regressions: a previous run leaves these files in its data/ dir, so passing that dir reproduces the exact failing inputs. |
save_data=True (default False) |
Writes the data/in/ + data/out/ snapshot so the exact inputs/goldens can be replayed later via golden_data. Off by default: runs skip the snapshot and validate against the in-memory golden only. Opt in when you need replay; full-model kernels like models/qwen3_14b/{prefill_fwd,decode_fwd}.py expose it as --save-data. |
For diagnosing compile errors, runtime hangs, and precision mismatches, see debugging.md.