Compile and Execution Entry Points¶
Which entry point does what, and which layer it belongs to.
PyPTO has more compile and execution entry points than it has concepts, and
several share a name across abstraction layers: six different functions are
called compile, and run is a method on both the runtime worker handles and
PassPipeline. This page is the map — what each entry point takes, what it
produces, and when to reach for it.
The four layers¶
Every entry point belongs to exactly one of four layers. Confusion arises when a name does not say which:
define compile artifact execute
────────────────────────────────────────────────────────────────────────────
@pl.program ──┐ CompiledProgram ChipWorker.run()
@pl.function ├─> ir.compile() ──> Distributed- DistributedWorker.run()
@pl.jit ──┘ kernel.compile() CompiledProgram compiled(*args)
kernel.lower() ──> ir.Program
Below the artifact layer sits an assembly layer — turning .pto and .cpp
into loadable binaries. It is internal and has no supported entry point.
Choosing an entry point¶
| You have | You want | Use |
|---|---|---|
A @pl.jit kernel and torch tensors |
The result | kernel(*args, config=...) |
A @pl.jit kernel |
The artifact, to dispatch repeatedly | kernel.compile(*args, config=...) |
A @pl.jit kernel |
The IR, without codegen | kernel.lower(*args) |
A @pl.program class |
The artifact | ir.compile(program, ...) |
A CompiledProgram |
One dispatch | compiled(*args, config=...) |
A CompiledProgram and device-resident data |
Many dispatches | ChipWorker.run(compiled, *args) |
A DistributedCompiledProgram |
Many dispatches | compiled.prepare() → DistributedWorker.run(...) |
| A build directory on disk | One dispatch, no recompile | CompiledProgram.from_dir(work_dir)(*args, config=...) |
A CompiledProgram |
Timed dispatches | benchmark(compiled, args, ...) |
Internal torch adaptation¶
The kernel-mode integration foundation validates NPU metadata and captures per-call frames. It does not change the public entry points above or provide a kernel launch interface.
Compile¶
| Entry | Layer | Location |
|---|---|---|
ir.compile |
compile driver | ir/compile.py |
JITFunction.compile |
specialize + driver | jit/decorator.py |
JITFunction.lower |
specialize only, stops at ir.Program |
same |
device_runner._compile_and_assemble |
assembly | runtime/device_runner.py |
device_runner._compile_single_kernel / _compile_single_orchestration |
assembly | same |
KernelCompiler.compile_incore |
ptoas invocation | runtime/kernel_compiler.py |
ir.compile is the only supported one; the rest of the table is here so a name
that turns up in a traceback can be placed on a layer. It also shadows the
Python builtin compile, so prefer from pypto import ir and call
ir.compile over importing the name directly. Every option it takes is
keyword-only; program is the one positional parameter.
Its parameters are documented in Compiling.
Both artifact types it can return, and the config that selects the distributed
one, are exported from pypto.ir:
The defining module, pypto.ir.distributed_compiled_program, stays importable —
pypto.runtime reaches for it directly to avoid an import cycle — but user code
and tests should take the three names from pypto.ir.
Execute¶
| Entry | Layer | Takes | Location |
|---|---|---|---|
CompiledProgram.__call__ |
artifact | artifact handle | ir/compiled_program.py |
Worker.run / ChipWorker.run / DistributedWorker.run |
execute | artifact + worker | runtime/worker.py |
CompiledProgram.from_dir / DistributedCompiledProgram.from_dir |
artifact | output directory | ir/compiled_program.py |
runtime.execute_compiled (deprecated) |
execute | output directory | runtime/runner.py |
execute_distributed_compiled (deprecated) |
execute | output directory | runtime/distributed_runner.py |
device_runner._execute_on_device |
assembly | assembled binaries | runtime/device_runner.py |
execute_artifact_dir / execute_batch_manifest |
CLI | output directory | runtime/execute_artifact.py |
run does not say which layer you are on; the receiver does. ChipWorker.run
and DistributedWorker.run dispatch an artifact, both implementing
Worker.run. PassPipeline.run is unrelated — it transforms a Program — as
is PassManager.run_passes.
Dispatching a directory rather than a live handle is what makes
replay possible: the PyPTO compile is skipped
entirely. from_dir is how you get there — it rebuilds the artifact handle
from the persisted sidecar, and calling that handle takes the same path as a
handle that never left memory.
execute_compiled and execute_distributed_compiled were the L2 and L3
spellings of that, and are deprecated. Each still forwards to the same
implementation, and each emits a DeprecationWarning.
The L3 one is a plain rename — it already was from_dir plus a call:
# before
execute_distributed_compiled(work_dir, args, config=cfg, platform="a2a3")
# after
from pypto.backend import BackendType
from pypto.runtime import ExecutionMode
ir.DistributedCompiledProgram.from_dir(
work_dir, backend_type=BackendType.Ascend910, execution_mode=ExecutionMode.ONBOARD,
)(*args, config=cfg)
The L2 one is not, because the two paths disagree on precedence:
| Setting | execute_compiled |
CompiledProgram.__call__ |
|---|---|---|
platform |
the explicit argument | config.platform when a config is passed, else the artifact's |
device_id / dfx / aicpu_thread_num |
the explicit arguments | always from config |
| ring overrides | from config |
from config |
So a config passed for ring sizing alone silently takes over the rest, and
RunConfig.platform defaults to a2a3sim — dropping the explicit arguments
would move the run to the simulator. Fold them into the config:
# before -- explicit args win; cfg was read for ring sizing only
execute_compiled(work_dir, args, platform="a2a3", device_id=0, config=cfg)
# after -- cfg carries every execution setting
from pypto.backend import BackendType
from pypto.runtime import ExecutionMode
cfg = dataclasses.replace(
cfg, backend_type=BackendType.Ascend910, execution_mode=ExecutionMode.ONBOARD, device_id=0,
)
ir.CompiledProgram.from_dir(work_dir)(*args, config=cfg)
dfx and aicpu_thread_num need no translation: the DFX toggles and
aicpu_thread_num are already RunConfig fields. The target passed to
from_dir(backend_type=..., execution_mode=...) applies to a call made
without a config.
Legacy platform examples¶
Some guides and runnable examples retain platform= to illustrate compatibility
with existing callers and platform-based test fixtures. Those examples are
explicitly marked legacy: the Python argument emits DeprecationWarning
and fails when that warning is treated as an error. For new Python code, import
BackendType from pypto.backend and ExecutionMode from pypto.runtime, then
replace platform= with both fields below:
Legacy platform |
backend_type |
execution_mode |
|---|---|---|
"a2a3" |
BackendType.Ascend910 |
ExecutionMode.ONBOARD |
"a2a3sim" |
BackendType.Ascend910 |
ExecutionMode.SIM |
"a5" |
BackendType.Ascend950 |
ExecutionMode.ONBOARD |
"a5sim" |
BackendType.Ascend950 |
ExecutionMode.SIM |
This applies to RunConfig, CompileOptions, ir.compile, both artifact
from_dir methods, and dataclasses.replace(config, ...). The CLI --platform
flag, warmup JSON, artifact metadata, and derived .platform properties retain
their existing string format and do not trigger the Python API deprecation.
Driving both phases from one config¶
RunConfig carries compile-time and dispatch-time settings together.
compile_kwargs() extracts the compile-time half as ir.compile keyword
arguments, so one config object drives both phases:
from pypto.backend import BackendType
from pypto.runtime import ExecutionMode, RunConfig
config = RunConfig(backend_type=BackendType.Ascend910, execution_mode=ExecutionMode.ONBOARD)
compiled = ir.compile(program, **config.compile_kwargs())
compiled(*tensors, config=config)
Its fields split three ways, and each way is a type:
| Read by | Type | Fields |
|---|---|---|
ir.compile |
CompileOptions |
backend_type, execution_mode (legacy platform), strategy, dump_passes, dump_ptoas_passes, profiling (compile_profiling), diagnostic_phase, disabled_diagnostics, analyze_auto_scopes_for_deps, output_dir (save_kernels_dir), memory_planner, distributed_config |
| Dispatch | RunOptions |
platform, device_id, aicpu_thread_num, the ring_* overrides (Ring sizing), and a nested DfxOptions |
| A dispatch's diagnostics | DfxOptions |
enable_chip_swimlane, enable_dump_args, enable_pmu, enable_dep_gen, enable_scope_stats (DFX) |
| The system-test harness only | — | rtol, atol, golden_data_dir, save_kernels, codegen_only |
| Nobody — derived | — | backend_type, RunConfig’s constructor/read alias over its arch storage |
Choose the target with backend_type and execution_mode. Architecture
selects compiler lowering and codegen; execution mode selects simulator or
onboard binary assembly and dispatch. Both RunConfig and CompileOptions
accept the pair, as does ir.compile directly:
from pypto.backend import BackendType
from pypto.runtime import CompileOptions, ExecutionMode, RunConfig
config = RunConfig(
backend_type=BackendType.Ascend950,
execution_mode=ExecutionMode.ONBOARD,
)
compiled = ir.compile(program, **config.compile_kwargs())
# Equivalent direct compilation:
compiled = ir.compile(
program, backend_type=BackendType.Ascend950, execution_mode=ExecutionMode.ONBOARD,
)
The default remains Ascend910 + SIM. Compiled programs expose both target
properties; from_dir(..., execution_mode=...) overrides the persisted mode,
and from_dir(..., backend_type=...) retains the persisted mode while updating
the backend and derived platform. Either axis can be overridden independently
without rewriting metadata. Existing runtime binding and cache restrictions
still apply. Reloading does not rerun code generation.
When selecting the target without a legacy platform, CompileOptions requires
an ExecutionMode enum value. Omit execution_mode to use SIM; explicitly
passing None or a string raises TypeError.
The platform= input is deprecated in RunConfig, CompileOptions (including
its positional form), ir.compile, CompiledProgram.from_dir, and
DistributedCompiledProgram.from_dir. These calls emit DeprecationWarning;
use backend_type and execution_mode instead. No removal version is scheduled.
Reading the derived .platform property remains silent, and RunConfig's arch
field remains compatible. A legacy platform still selects both axes and wins over backend_type;
RunConfig retains its warning when those two disagree. ir.compile rejects an
explicit execution_mode that contradicts an explicit platform. In RunConfig and
CompileOptions the legacy platform wins over both axes, preserving
dataclasses.replace(config, platform=...). Without platform, backend_type now
selects the architecture (it is no longer discarded by RunConfig).
RunConfig temporarily stores its architecture as arch; backend_type is its
constructor/read alias. CompileOptions stores backend_type and execution_mode.
Their compile mappings forward backend_type and execution_mode, so new-style
compilation does not emit a legacy warning. Artifact sidecars, CLI compatibility
paths, cache keys, and Simpler SDK calls retain the canonical internal platform
encoding. New and
legacy requests therefore identify the same cache entry. This change does not
alter per-call config defaulting or the process-global backend selection rule.
RunConfig.compile_options() / run_options() / dfx_options() are views onto
the aggregate, and compile_kwargs() is compile_options().as_compile_kwargs().
CompileOptions names its fields the way ir.compile does — output_dir, not
save_kernels_dir — because it exists to say the compile side in the compiler's
own vocabulary, and it stands alone:
from pypto.runtime import CompileOptions, ExecutionMode
compiled = ir.compile(program, **CompileOptions(execution_mode=ExecutionMode.ONBOARD).as_compile_kwargs())
Only two of the three are exported, because only two are things a caller can
hand somewhere. CompileOptions unpacks into ir.compile, above.
DfxOptions is the dfx= parameter of execute_compiled, execute_artifact_dir
and execute_batch_manifest. RunOptions is neither: every dispatch entry point
— CompiledProgram.__call__, ChipWorker.run, and the distributed pair — takes
a RunConfig and reaches the dispatch half through run_options() itself.
Handing one in raises AttributeError, so it stays internal
(pypto.runtime.runner.RunOptions) until those signatures widen. Widening them
is a migration of its own: it means a union type on the config= parameter of
the primary dispatch API, and doing it to some entry points and not others would
be worse than not doing it.
RunConfig keeps every field and every caller; the three types are the
vocabulary underneath it. A unit test pins the split as total — every
RunConfig field is claimed by one of the views or is one of the five
harness-only fields — so a field added later cannot quietly belong to neither.
Moving those five out to the harness is the step this does not take: they reach
pypto-lib's constructor calls too.
compile_kwargs() is the only mapping onto ir.compile's parameters. The
@pl.jit path used to carry a second copy that omitted platform and
backend_type and forwarded the platform separately; it now calls
compile_kwargs() like every other caller, so a knob added to one is not
silently missing from the other. lower() still has its own narrower mapping —
it stops before codegen and targets the pass pipeline, not ir.compile.
Which way the layers point¶
ir produces the artifact; runtime dispatches it. The arrow between them
should point one way, and mostly does — but CompiledProgram is both the
compilation artifact and the execution handle, so pypto.ir.compiled_program
reaches forward into pypto.runtime through ten function-local imports.
Those deferrals are usually described as breaking an import cycle. Measured, one at a time, that was true of exactly one of them:
| Deferred import | Hoists cleanly? | Actual reason |
|---|---|---|
runtime.runner (6 sites) |
Yes | Layering choice — keeps the dispatch layer out of pypto.ir's import graph |
runtime.distributed_runner (2 sites) |
Yes | Same |
runtime.debug.run_script_writer |
Yes, now | Was a real cycle: it read ParamInfo back out of compiled_program |
runtime.device_runner |
No | Needs the optional simpler package at import time |
The one real cycle is gone. The parameter metadata moved to
ir/param_info.py, a leaf that imports
nothing from pypto.runtime, and the replay-script writer reads it there;
compiled_program re-exports the names, so nothing else moved. A unit test pins
the leaf, because pulling one runtime import into it restores the cycle.
pypto.runtime importing pypto.ir.distributed_compiled_program directly — the
one place the arrow points back — is cycle-avoidance, and stays.
Fully inverting the dependency is a larger question than the import statements.
It means CompiledProgram stops being callable and ir.compile returns a
descriptor that runtime wraps, which changes the return type of the API every
example and both downstream repositories use. The import list is a symptom of
that double role, not the cause, and hoisting the eight that hoist cleanly would
make the coupling stronger, not weaker — it would put pypto.runtime.runner
on import pypto.ir's critical path.
What is internal¶
These have no stability guarantee. They are not in any __all__, and code
outside PyPTO should not import them:
pypto.ir.compile—_ensure_orchestration_headerspypto.ir.compiled_program—CompiledProgram._build_orch_args,CompiledProgram._build_call_configpypto.runtime.runner—_execute_compiled,_execute_golden_case,_build_call_config,_coerced_to_orch_args,RunOptionspypto.runtime.distributed_runner—_execute_distributedpypto.runtime.device_runner— the whole assembly layer:_compile_and_assemble,_compile_single_kernel,_compile_single_orchestration,_execute_on_devicepypto.runtime.kernel_compiler—KernelCompiler,compile_incorepypto.runtime.tensor_arg,pypto.runtime.elf_parser,pypto.runtime._binary_cache
The assembly layer carries the underscore so a traceback says which side of the
boundary it came from: an import that has to spell _ is an import that had to
decide to. KernelCompiler / compile_incore and the three modules on the last
line are the exceptions — they are internal by module rather than by name, and
carry the same absence of a stability guarantee.
The two argument builders marshal user arguments into simpler's TaskArgs and
CallConfig. ChipWorker.run and ChipWorker.register are the supported way
to reach that path — they call the builders for you, and a caller driving a
hand-constructed simpler.worker.Worker still gets chip_callable,
runtime_name and runtime_config from the public surface.
Command-line entry points¶
| Command | Purpose |
|---|---|
pypto-ir-trace |
Render a pass dump as an interactive lowering trace (IR lowering trace) |
python -m pypto.runtime.execute_artifact |
Execute an artifact directory or a batch manifest |
python -m pypto.runtime.debug.replay |
Re-run a build directory after hand-editing generated code (Replay) |
python build_output/<dir>/debug/run.py |
The self-contained reproducer emitted with every build |
python -m pypto.tools.memory_map |
Render on-chip memory as HTML (Memory Map) |
python -m pypto.tools.clean_sim_trace |
Convert simulator dumps to readable traces (Trace cleaning) |
See Also¶
- Compiling —
ir.compile's parameters and the artifact directory. - Running —
CompiledProgram,ChipWorker,DeviceTensor. - Runtime DFX Flags — the five diagnostic sub-features.
- Per-Task Ring Sizing — the three per-dispatch ring overrides.