Distributed Architecture Design¶
This section introduces the overall architecture of Triton-distributed-ascend on the Ascend platform: from Python distributed primitives to NPU hardware execution, covering the IR levels involved, the components responsible for each, and how the symmetric memory runtime supports cross-device communication.
For definitions of professional terms and abbreviations used in this document, see Glossary.
Overall Architecture¶
Layered Design¶

Core Design Philosophy: Distributed Dialect is a platform-agnostic intermediate representation. Each platform maps it to its own communication library through different Conversion Passes, and business-side dl.* code does not need to be rewritten for different platforms.
The landing point for the Ascend platform is the hivm.custom operation—each Distributed Dialect operation is converted into a custom op with a symbol name, where the symbol statically exists in the device-side template library and is resolved by the backend during linking. No runtime module patching is required after compilation, which is fundamentally different from the A/B paths (see Runtime Implementation of Communication Primitives).
Architectural Differences from the GPU Path¶
GPU |
Ascend NPU |
|
|---|---|---|
How communication library symbols are bound |
After compiling the binary, the host side patches the SHMEM context pointer into the device module |
During compilation, |
How the device side finds communication state |
Through a global variable patched in |
Located at a fixed GM address in the architecture, no parameter passing needed |
Runtime initialization overhead |
Module patching required every time it’s loaded |
None |
This difference determines that operations like dl.rank() on Ascend can be implemented as pure functions without additional parameters—the address of the device state is a compile-time known constant.
Distributed Dialect¶
Dialect Definition¶
See DistributedOps.md for the Distributed Dialect specification.
Memory Side Effect Model¶
Side effects are not declared in TableGen, but implemented in C++ via MemoryEffectsOpInterface:
Op |
Side Effects |
|---|---|
|
Only |
|
Empty—purely an IR dependency anchor |
|
Only |
|
|
|
No side effects when |
ConvertTritonDistributedToHIVM Pass¶
Pass Overview¶
Pass name:
convert-triton-distributed-to-hivmScope:
ModuleOpDependent dialects:
hivm::HIVMDialect,triton::distributed::DistributedDialect
Driver Logic¶
The runOnOperation() of the Pass does two things:
Scan for the presence of
triton::DotOp/DotScaledOp, obtaining anexistDotflag. The absence oftl.dotindicates this is a pure AIV kernel, and this information is used for subsequent core type inference.Register rewrite patterns for the seven distributed ops.
aclshmem Interface OP Generation¶
The conversion template class generates a symbol name for each operation:
Source op |
Corresponding aclshmem interface name |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Uses the op’s own |
Attribute Configuration¶
TCoreType (determines which type of core this operation executes on):
The Triton-distributed Python frontend annotates each externalCallOp with a distributed.core_type string attribute, which is used to derive the hivm::TCoreType attribute consumed directly by downstream passes.
|
|
|---|---|
|
CUBE |
|
VECTOR |
|
CUBE_AND_VECTOR |
|
Currently inferred from |
For an externalCallOp, if the distributed.core_type attribute is missing or has an unrecognized value, the pass reports an error immediately. This ensures that the Python frontend must be updated together with the pass; otherwise, the pass could infer an incorrect TCoreType.
The annotation is currently emitted by libaclshmem_device primitives. A distributed.extern_call produced through any other path must also specify distributed.core_type; otherwise, it cannot pass this conversion.
The core type for each primitive is shown below:
Python primitive |
|
|---|---|
|
|
|
|
rma & udma apis |
|
others |
|
The marking of aclshmem_barrier_all as CUBE_AND_VECTOR is important: it means the barrier exists in both functions simultaneously after the kernel is split into cube func and vector func, which is the mechanism that enables computation-communication overlap.
distributed.core_type is a frontend-only annotation: once the op has been lowered to hivm.custom, the pass removes the attribute, leaving only the resolved hivm.tcore_type.
Other Attributes:
Attribute |
Value |
Description |
|---|---|---|
|
|
Set unconditionally |
|
|
Set unconditionally |
|
UnitAttr |
Downstream marker for identifying distributed operations in multiple subsequent stages such as memory scope inference, block pointer analysis, and mix kernel splitting |
|
StrAttr |
Bare symbol name |
|
UnitAttr (conditional) |
Set only when the source op’s memory side effects are empty |
|
DenseI32Array |
Collects the operand indices that are both pointer types and entry block arguments of |
Result Construction and Replacement¶
For each RankedTensorType result, first create a tensor::EmptyOp as the init value for DPS (destination-passing style), then construct hivm::CustomOp. Finally: if no results, eraseOp; if results, replaceOp.
Compilation Pipeline¶
Complete Pipeline and Stage Entry Points¶
Python AST → Triton IR (generate distributed.* operations using DistributedOpBuilder)
│ builder bindings: create_distributed_wait / consume_token / get_rank /
│ get_num_ranks / symm_at / notify / extern_call
↓
【stage "ttir"】
Standard Triton optimizations: inliner / combine / canonicalize / cse / licm / loop_unroll
↓
【stage "ttadapter"】
add_convert_triton_distributed_to_hivm(pm)
—— Executes before all subsequent passes of single-device compilation,
distributed operations have already become hivm.custom before entering TTIR→Linalg conversion
↓
【stage "npubin"】
Single-device NPU compilation and binary generation
(TTIR → Linalg IR → AscendNPU IR → machine code)
↓
Ascend NPU executable kernel
How Symbols Become Real Code (AscendNPU-IR)¶
This is the final link in understanding the entire architecture: the Pass only generates a string symbol name, how does it become executable code?
Step 1: Symbol Name Mangling
The mlir_ciface prefix is added; that is, symbol = "aclshmem_my_pe" → link name _mlir_ciface_aclshmem_my_pe.
Step 2: Static Implementation in Template Library
Every symbol generated by the Pass has a corresponding _mlir_ciface_ implementation in the device-side template library. A few representative examples:
_mlir_ciface_aclshmem_my_pe/_mlir_ciface_aclshmem_n_pes—— Directly read device stateACLSHMEM_PTR_WRAPPERmacro —— Expands to 12 type variants ofaclshmem_ptr_<T>, wrapping the result ofaclshmem_ptr()back into a memref structureACLSHMEM_P_WRAPPERmacro —— Expands toaclshmem_int64_p/uint64_p, etc.ACLSHMEM_WAIT_WRAPPERmacro —— Expands toaclshmem_wait_int32/64, etc.
The implementation body of CONSUME_TOKEN_*_WRAPPER simply returns the input as-is, serving as a barrier to create data dependencies for the compiler, with no runtime overhead.
Step 3: Compile to Bitcode and Link
The template library source code is compiled into four variants (aic / aiv / mix_aiv / mix_aic) of bitcode, which are linked in when generating the final kernel binary.
This is the complete answer to “why the Pass only needs to generate a string symbol name”: the symbol statically exists in the template library and is resolved by the backend during linking.
Cross-platform Dispatch Mechanism¶
Dispatch Structure¶
dl.wait / dl.notify / dl.symm_at / dl.rank / dl.num_ranks / dl.consume_token
→ Static, no proxy → _builder.create_* → distributed dialect → (Ascend) HIVM Pass
※ Only backend conditional branch: dl.notify dtype validation
libshmem_device.<f> → ModuleProxy dynamic dispatch
→ (Ascend) libaclshmem_device.<f> → extern_call → ExternCallOp → aclshmem_* symbol
Core dl.* primitives take the static path—they directly create MLIR operations, with platform differences entirely handled by Conversion Passes; the Python layer has no branches. This is the foundation of cross-platform portability.
libshmem_device.* takes the dynamic path—because different platforms have non-identical SHMEM library function sets, requiring runtime platform-based module selection.
ModuleProxy¶
The only dispatch primitive is ModuleProxy, with simple logic:
During construction, evaluate
[(predicate, module), ...]one by one, asserting exactly one predicate is true;__getattr__transparently forwards attribute access to the selected module;The
dispatchdecorator makes the decorated function look up the same-named function in the active module at call time byfunc.__name__.
Backend Information Retrieval¶
Backend determination is entirely based on shutil.which(<vendor smi tool>):
Function |
Criterion |
|---|---|
|
|
|
|
|
|
|
|
Ascend Branch Checklist¶
Location |
What the Ascend Branch Does |
|---|---|
|
Adds |
|
Adds |
|
|
|
Ascend branch empty during import, does not import any vendor SDK |
Compiler |
Inserts HIVM Pass in the |
pybind layer |
Mounts |