L2 Cache Bypass: Native CANN and simpler Delivery Paths¶
This document explains how an A2/A3 load marked for L2 bypass obtains the device-specific nocache-alias offset, why the native AscendC/CANN mechanism does not work for a simpler incore kernel, and how simpler transports the same driver-owned value instead.
The runtime transport described here is implemented by
PR #2323. The compiler
side must still consume get_l2_cache_offset(args) before
pl.CachePolicy.BYPASS is end-to-end usable on simpler; see
PTOAS #1537.
The two independent decisions¶
L2 bypass has two inputs with different owners:
| Input | Meaning | Owner | Lifetime |
|---|---|---|---|
| Load policy | Which individual GM loads should not allocate in L2 | frontend and PTOAS lowering | compiled into the kernel's .text |
| Nocache offset | How this device reaches the nocache alias of a GM address | driver and runtime | one value per device, fixed for a Worker lifetime |
The offset does not decide whether a load bypasses L2. PTOAS makes that decision per load. The offset only supplies the target-specific address transformation needed by loads already lowered as bypass loads.
It also does not vary per task or per incore kernel. Native CANN stores one
copy in every registered kernel image, but those copies contain the same
per-device value. simpler stores one copy in each core's GlobalContext and
shares it across every incore kernel dispatched to that core.
Why adding an offset changes the cache policy¶
On devices that expose the A2/A3 mechanism, the driver maps the same physical GM pages through two virtual-address windows:
addr + offset therefore names the same bytes through the nocache alias. It
is not an access to different physical data. A load through that alias uses the
not-allocate cache behavior, which avoids filling L2 with data that will not be
reused.
The offset is a driver-owned virtual-address-layout value. The runtime API is deliberately keyed by physical device ID:
Code must query it rather than infer it from an architecture name or hardcode
a value. For example, the value observed during PR #2323 validation was
0x80000000000, while a pto-isa comment named 0x100000000000. A static
constant would silently address the wrong window on at least one environment.
An offset of zero means that no alias is available. Adding zero preserves the ordinary address, so lack of support or a failed query safely loses only the optimization and leaves the load correct and cached.
A2/A3 only¶
The offset exists in the A2/A3 trees alone. A2/A3's DMA load instruction does
not carry the cache policy as an operand, so pto-isa reaches the uncached
mapping by offsetting the address instead. A5's TLOAD does carry l2Control
as an instruction operand, and pto-isa's TLoadSrcAddrWithL2Hint returns the
address unchanged on every non-A2/A3 architecture — an A5 kernel could not
consume an offset it was handed. So InitArgs, GlobalContext, and the
accessor all stay A2/A3-only rather than mirroring a field no kernel reads.
Native AscendC and CANN path¶
The native path is a three-party contract between PTOAS, pto-isa, and the CANN runtime loader.
Compile time¶
PTOAS lowers a frontend policy such as
#pto.load_cache_policy<l2_bypass> to a load resembling:
On A2/A3, pto-isa then emits three related pieces into the kernel ELF:
| ELF section | Content | Purpose |
|---|---|---|
.text |
A bypass load reads g_opL2CacheHintCfg.l2Cacheoffset and adds it to its source address |
Records which loads use the alias |
.data |
g_opL2CacheHintCfg, initialized to zero |
Provides a slot for the loader-supplied value |
.ascend.meta |
Runtime implicit-information record {4, 4, 3}, where feature 3 is L2CACHE |
Tells the CANN loader that the slot must be processed |
The metadata record does not mean "all loads in this kernel bypass L2." The
per-load lowering in .text carries that choice. The metadata only advertises
the presence of a runtime configuration slot. It can be emitted even when no
load ultimately uses the bypass policy.
Binary registration¶
When CANN registers the complete ELF, its host-side loader:
- parses
.ascend.metaand recognizes theL2CACHEfeature; - locates
g_opL2CacheHintCfgthrough the ELF symbol table; - calls
rtGetL2CacheOffset(device_id, &offset); - writes the offset into the kernel image's
.dataslot in the host buffer; - uploads the complete patched image.
The query and patch occur before the AICore starts. The AICore cannot call the driver: it has no host operating system, system-call path, or access to the host runtime API.
Kernel execution¶
At execution time a bypass load performs an ordinary device-memory read of the already-patched slot, adds the value to its source address, and issues the load against the nocache alias. A regular load leaves the address unchanged.
PTOAS policy
-> pto-isa emits .text + .data slot + .ascend.meta
-> CANN loader parses, queries the driver, and patches the slot
-> complete ELF is uploaded
-> AICore load reads the slot and adds the offset
Why that ABI breaks for simpler incore kernels¶
simpler's outer dispatcher and its dynamically dispatched incore kernels use different loading mechanisms. The native CANN contract applies to the former, not the latter.
| Property | CANN-registered kernel | simpler incore kernel |
|---|---|---|
| Loader input | Complete linked ELF | Linked .text bytes only |
| ELF registration | CANN sees and registers the image | No CANN binary registration |
.ascend.meta |
Parsed | Discarded |
.data |
Patched and uploaded | Discarded |
| Entry | CANN launch | Runtime casts the payload address to kernel(args) |
extract_text_section deliberately extracts
only the linked .text payload. The broader loader contract is described in
AICore Kernel Programming.
For g_opL2CacheHintCfg, this has two consequences:
- CANN never sees the incore ELF, so it cannot parse the metadata, query the driver for this binary, or patch its symbol.
- The initialized
__gm__variable lives in.data, which is not copied to the device. The linked.textstill contains a PC-relative reference to the expected location, so it reads unrelated device memory rather than the safe zero initializer.
The resulting address is valid_addr + garbage, which explains both observed
failure modes: silent wrong data when the accidental address is readable and
an AICore fault when it is not. Moving the metadata to the dispatcher would
not help; the dispatcher and each incore payload are separate images, and
CANN would patch the dispatcher's slot rather than storage belonging to the
incore payload.
This is also why the current design does not claim to fix initialized __gm__
globals in general. Full data-section loading is a separate loader problem.
simpler runtime delivery path¶
simpler keeps the authoritative source, rtGetL2CacheOffset, but replaces
CANN's ELF-patching transport with the existing runtime context transport used
for other device configuration such as async-DMA workspaces.
host: rtGetL2CacheOffset(device_id)
-> InitArgs.l2_cache_offset
-> resident AICPU configuration
-> scheduler cold start
-> per-core GlobalContext.l2_cache_offset
-> args[SPMD_GLOBAL_CONTEXT_INDEX]
-> get_l2_cache_offset(args)
-> generated incore kernel's bypass-load lowering
The stages are:
DeviceRunner::fill_init_arch_fieldsqueries the driver immediately before the one-time AICPU initialization. Unsupported devices and query errors are logged and represented as zero. It sits on the A2/A3 runner rather than the shared base because A5'sInitArgshas no such field — the same splitkernel_args_init_ffts_base_addruses.InitArgscarries the value intosimpler_aicpu_initonce per Worker/device initialization.- The resident AICPU SO stores it through
set_dev_l2_cache_offset. - Each runtime's scheduler cold path copies the resident value into every
core's
GlobalContext, for AIC as well as AIV cores. - Each dispatch already passes that context through the fixed tail of
args[]. The incore accessorget_l2_cache_offset(args)reads the field.
args is used because it is the existing channel that reaches a dynamically
loaded incore kernel, not because the value changes on every dispatch. The
query occurs once, the cold-start copy occurs once per core, and all later
tasks on that Worker read the same value.
Simulation needs no extra wiring. The shared resident configuration is
zero-initialized, no device query overwrites it, and addr + 0 remains an
ordinary cached access.
What holds the chain together¶
tests/st/a2a3/tensormap_and_ringbuffer/spmd_basic reads the accessor from all
three cores of a MIX task and records, per core, whether the offset it saw was
nonzero. Its onboard case expects every one of them — including the AIC — to see
one, which is what catches the cold-start copy being narrowed to the AIV pair
that sub_block_id walks. The sim case expects zero, pinning the args index
and the GlobalContext layout agreeing between the AICore-side and AICPU-side
headers. Neither case can assert the offset's value: it is whatever the driver
reports for that device.
Compiler handoff still required¶
PR #2323 ends at get_l2_cache_offset(args). It intentionally does not change
PTOAS or pto-isa, so the existing A2/A3 lowering still reads
g_opL2CacheHintCfg until the compiler stack adopts this contract.
The generated incore kernel must:
- read
get_l2_cache_offset(args)once atkernel_entry; - make that value available to the A2/A3 address-hint lowering, either by threading the local value through generated code or by initializing a block-local slot at entry;
- add it only for loads whose compiled policy is a not-allocate policy; and
- stop depending on the initialized
__gm__global and its CANN-only.ascend.metapatch path for simpler incore payloads.
Conceptually, the generated code becomes:
extern "C" __aicore__ void kernel_entry(__gm__ int64_t *args) {
const uint64_t l2_cache_offset = get_l2_cache_offset(args);
// ...
// Only a load compiled with the bypass policy uses base + l2_cache_offset.
}
Reading once at entry matters: calling the accessor at every load would
re-read GlobalContext through GM even though the value cannot change during
the dispatch. A block-local handoff is compatible with simpler's text-only
payload because block-local storage is allocated by the AICore environment; it
is not an initialized .data section that the incore loader must upload.
Until this compiler handoff lands, the runtime accessor is available and has
been validated, but the original pl.CachePolicy.BYPASS lowering remains on
the incompatible CANN path.