Platform
a2a3 (Ascend 910B/C hardware)
Runtime Variant
tensormap_and_ringbuffer
Summary
Tracking issue for the a2a3 tensormap_and_ringbuffer orchestrator. Captures a PTO2_ORCH_PROFILING=1 measurement baseline of the per-task submit hot path, side-by-side numbers for the auto-dependency (paged_attention_unroll) and manual-dependency (paged_attention_unroll_manual_scope) variants on the same workload, and the architectural background needed to interpret them.
The intent of this issue is characterization, not regression reporting. Concrete evolution directions are posted as separate comments.
Related: #545 (overall runtime perf tracking — scheduler / dispatch / runtime-wrapping side), #849, #902.
Git Commit ID
60742ff
CANN Version
9.0.0 (V100R001C10SPC001B250)
Driver Version
26.0.rc1 (ascendhal_version 7.35.23)
Host Platform
Linux (aarch64)
Reproduction
# 1. Enable orchestrator + tensormap profiling
# (src/a2a3/runtime/tensormap_and_ringbuffer/runtime/pto_runtime2_types.h)
# #define PTO2_ORCH_PROFILING 1
# #define PTO2_TENSORMAP_PROFILING 1
# 2. Rebuild the a2a3 onboard runtime
python3 -m venv --system-site-packages .venv && source .venv/bin/activate
pip install --no-build-isolation -e .
python simpler_setup/build_runtimes.py --platforms a2a3
# 3. Run the auto-dep variant on a free a2a3 device
task-submit --device auto --device-num 1 --run "source .venv/bin/activate && \
python -m pytest \
tests/st/a2a3/tensormap_and_ringbuffer/paged_attention_unroll/test_paged_attention_unroll.py \
--platform a2a3 --device \$TASK_DEVICE -v -s --log-level v9 --enable-l2-swimlane 4"
# Inspect device log for the '=== Orchestrator Profiling' block:
ls -lt $HOME/ascend/log/debug/device-<id>/ | head -3
# 4. Repeat with the manual-scope variant
task-submit --device auto --device-num 1 --run "source .venv/bin/activate && \
python -m pytest \
examples/a2a3/tensormap_and_ringbuffer/paged_attention_unroll_manual_scope/test_paged_attention_unroll.py \
--platform a2a3 --device \$TASK_DEVICE -v -s --log-level v9 --enable-l2-swimlane 4"
Both tests use the same Case1 parameters (batch=256, num_heads=16, kv_head_num=1, head_dim=128, block_size=128, context_len=8192, max_model_len=32768, dtype=bfloat16, aicpu_thread_num=4, block_dim=24).
Additional Context
1. What the orchestrator does
Single AICPU thread (Thread 3 of 4 on a2a3). Per runtime/pto_orchestrator.h:11-26: executes the user orchestration function, allocates intermediate buffers from the GM heap (heap ring), submits tasks via PTO2OrchestratorState::submit_task (real work), submit_dummy_task (barrier), or alloc_tensors (inline-complete buffer carve-out), builds the dependency graph through TensorMap or explicit_deps, and manages scopes via PTO2_SCOPE. It is not an executor — it writes records into shared memory and pushes opaque slot-state pointers onto a single SPSC wiring queue; the scheduler threads do all fanout wiring, list maintenance, and watermark advancement.
2. Two components: runtime interface and user code
The runtime is split into a function-pointer ops table and the user .so that calls it through that table. There is no link dependency: AICPU Thread 3 dumps the orchestration .so to a temp file, dlopens it, looks up aicpu_orchestration_config and aicpu_orchestration_entry, the runtime populates rt->ops, and the user code calls back via current_runtime()->ops->submit_task (etc.).
- Runtime interface:
src/a2a3/runtime/tensormap_and_ringbuffer/orchestration/pto_orchestration_api.h — opaque struct PTO2Runtime { const PTO2RuntimeOps *ops; PTO2ScopeMode pending_scope_mode; }, the PTO2RuntimeOps function table (submit_task, scope_begin/end, alloc_tensors, submit_dummy_task, logging, set_tensor_data/get_tensor_data, fatal handling), inline rt_submit_*_task wrappers, the Arg builder, and PTO2_SCOPE(...) RAII.
- User code:
examples/.../paged_attention_orch.cpp exports exactly aicpu_orchestration_entry (and _config). Includes only pto_orchestration_api.h. Zero runtime symbol dependencies.
3. Six phases of one task submission
All happen inside submit_task_common (runtime/pto_orchestrator.cpp:507-688), instrumented by CYCLE_COUNT_LAP calls:
- Alloc (
g_orch_alloc_cycle) — task ring slot allocation, heap ring packed-output allocation, prefetch_payload, bind_buffers, task_state.store(PENDING), scope_tasks_push. Blocks on back-pressure here.
- sync_tensormap (
g_orch_sync_cycle) — acquire-load fc.last_task_alive, possibly cleanup_retired every PTO2_TENSORMAP_CLEANUP_INTERVAL=64 retired tasks.
- lookup+dep (
g_orch_lookup_cycle) — explicit_deps loop (retire-skip + fanin append) and compute_task_fanin (pto_dep_compute.h:81-129): per non-OUTPUT tensor, Step A creator retention from tensor->owner_task_id, then Step B tensor_map.lookup — hash-bucket walk + check_overlap cascade (L1 byte-range, L2 hyper-rectangle, L3 conservative-OTHER). INOUT+COVERED removes the entry. Inline fanin builder caps at PTO2_FANIN_INLINE_CAP, spills to fanin_pool above that.
- tensormap_ins (
g_orch_insert_cycle) — register_task_outputs (pto_dep_compute.h:140-154): for each INOUT and OUTPUT_EXISTING, allocate entry from free list or bump region, 64-byte copy_from_tensor memcpy, hash-bucket and per-task chain prepends.
- param_copy (
g_orch_args_cycle) — task.task_id / kernel_id[3] / packed_buffer_* writes, per-producer fanout_count++, fanin metadata into payload, and payload.init(args, result, alloc_result, layout) — the actual tensor + scalar deep copy into the GM payload that AICore will read.
- fanin+ready (
g_orch_fanin_cycle) — single SPSC push of the slot-state pointer to sched->wiring.queue. The actual fanout wiring (lock + dep_pool prepend + early-finished check + ready-queue push) is deferred to scheduler thread 0's drain_wiring_queue (scheduler/pto_scheduler.h:671-711).
scope_end cost is tracked separately (g_orch_scope_end_cycle) — iterates scope tasks and calls scheduler->on_scope_end to release the +1 scope reference on each producer's fanout_count.
Raw device-log excerpts on the 1280-task Case1 workload (verbatim, paths anonymized):
# paged_attention_unroll (auto-dep, PTO2_SCOPE())
Thread 3: === Orchestrator Profiling: 1280 tasks, total=646.960us ===
Thread 3: task+heap_alloc: 171.280us (26.5%) work=171.280us wait=0.000us atomics=1280
Thread 3: sync_tensormap : 56.400us (8.7%)
Thread 3: lookup+dep : 80.720us (12.5%)
Thread 3: tensormap_ins : 70.520us (10.9%)
Thread 3: param_copy : 235.600us (36.4%) atomics=2048
Thread 3: fanin+ready : 32.440us (5.0%) work=32.440us wait=0.000us
Thread 3: avg/task : 0.505us
Thread 3: === TensorMap Lookup Stats ===
Thread 3: lookups : 3584, inserts: 768
Thread 3: chain walked : total=11, avg=0.0, max=1
Thread 3: overlap checks : 0, hits=0 (0.0%)
Thread 3: PTO2 total submitted tasks = 1280, already executed 1256 tasks
# paged_attention_unroll_manual_scope (manual deps, PTO2_SCOPE(PTO2ScopeMode::MANUAL))
Thread 3: === Orchestrator Profiling: 1280 tasks, total=486.820us ===
Thread 3: task+heap_alloc: 161.580us (33.2%) work=161.580us wait=0.000us atomics=1280
Thread 3: sync_tensormap : 33.080us (6.8%)
Thread 3: lookup+dep : 19.360us (4.0%)
Thread 3: tensormap_ins : 3.520us (0.7%)
Thread 3: param_copy : 252.120us (51.8%) atomics=2048
Thread 3: fanin+ready : 17.160us (3.5%) work=17.160us wait=0.000us
Thread 3: avg/task : 0.380us
Thread 3: === TensorMap Lookup Stats ===
Thread 3: lookups : 256, inserts: 0
Thread 3: chain walked : total=0, avg=0.0, max=0
Thread 3: overlap checks : 0, hits=0 (0.0%)
Thread 3: PTO2 total submitted tasks = 1280, already executed 1051 tasks
4. TensorMap cost and sequentiality
The orchestrator is one thread, so steps 2/3/4 run strictly in order with no pipelining: N tasks back-to-back cost N×(sync + lookup + insert). The TensorMap is private to the orchestrator (no atomics), so there's no contention — but also no parallelism. Auto-dep mode runs compute_task_fanin and register_task_outputs; manual-dep mode short-circuits both (pto_dep_compute.h:84-86 and :142-144). That's why the manual run shows inserts=0 and lookups=256 (only one trivial lookup per submit, no chain walked, no overlap check).
Side-by-side per-phase breakdown on the same 1280-task Case1 workload, same commit, both runs on a free a2a3 device under task-submit exclusive lock:
| Phase |
unroll (auto-dep) |
unroll_manual_scope |
Δ |
total (sum of phases) |
646.96 us |
486.82 us |
-24.8% |
avg/task |
0.505 us |
0.380 us |
-24.8% |
task+heap_alloc |
171.28 us (26.5%) |
161.58 us (33.2%) |
-5.7% |
sync_tensormap |
56.40 us (8.7%) |
33.08 us (6.8%) |
-41.3% |
lookup+dep |
80.72 us (12.5%) |
19.36 us (4.0%) |
-76.0% |
tensormap_ins |
70.52 us (10.9%) |
3.52 us (0.7%) |
-95.0% |
param_copy |
235.60 us (36.4%) |
252.12 us (51.8%) |
+7.0% |
fanin+ready |
32.44 us (5.0%) |
17.16 us (3.5%) |
-47.1% |
TensorMap-internal stats:
| Stat |
unroll (auto-dep) |
unroll_manual_scope |
| lookups |
3584 |
256 |
| inserts |
768 |
0 |
| chain walked total / avg / max |
11 / 0.0 / 1 |
0 / 0.0 / 0 |
| overlap checks / hits |
0 / 0 |
0 / 0 |
Observations:
param_copy stays the largest single phase in both runs (36-52%) — the unavoidable Tensor + scalar deep copy into the GM payload, tensormap-independent.
- Eliminating TensorMap collapses two phases (
lookup+dep + tensormap_ins) from a combined 151.24 us (23.4%) to 22.88 us (4.7%), a 128.36 us / 19% absolute drop on total.
sync_tensormap does not go to zero in manual mode because it is unconditional (pto_orchestrator.cpp:574-578) — it still loads last_task_alive and may run cleanup_retired even with zero inserts.
fanin+ready drops 47% — orch is pushing fewer slot states through the SPSC wiring queue because explicit deps don't synthesize redundant edges.
task+heap_alloc is essentially flat — the ring buffers are uncontested at this 1280-task / 16K-window scale.
- The 3584 lookups in the auto run come from the 4-tensor-input per submit × 1280 submits × ~0.7 non-OUTPUT-tensor fraction. Even with max chain length of 1 and zero overlap checks (this workload's tensors don't share base addrs in the same hash bucket — unsurprising for paged attention), the per-lookup cost is non-trivial because
lookup always pays the hash + cache-line fetch + iterate cost.
5. Orch ↔ scheduler interaction factors
Two distinct cost regimes for orch↔sched data sharing on the hot path:
- Factor A — Write-once-by-orch, read-by-sched payload (capacity-bound, small):
PTO2TaskDescriptor, PTO2TaskPayload, and the per-slot PTO2TaskSlotState payload/task pointers are written once by the orchestrator at step 5, read once by the scheduler at dispatch (and again by AICore at execute). No coherence ping-pong: orch dirties, sched reads. Per-task payload is bounded (≤16 tensors + ≤16 scalars), and active payload working set fits in L2 by design — task_window_size is small, default 16384/ring, often as small as 16. Cost: bandwidth only.
- Factor B — Shared coordination cache lines (coherence-bound, big): lines that both threads write or one writes and the other reads on the hot path force snoop / invalidation round-trips. The hot lines are:
PTO2RingFlowControl (pto_shared_memory.h:54-76) — current_task_index and last_task_alive are on separate 64B-aligned lines on purpose, so each is single-writer.
PTO2TaskSlotState (pto_runtime2_types.h:316-398) — single 64B line carrying both orch-managed (task_state=PENDING, bind_buffers, active_mask, fanout_count increments on producers) and sched-managed (task_state=COMPLETED/CONSUMED, fanout_refcount, fanin_refcount, fanin_count) fields. Pings on every fan-out edge.
sched->wiring.queue slot lines — orch pushes (step 6), sched batch-pops (drain_wiring_queue). One ping per submit.
Factor B is what scales with fan-out density. The recent deferred-wiring redesign (orch no longer takes fanout_lock per producer, no longer allocates dep_pool) was a targeted cut on this; the remaining orch-side coherence work is the per-producer fanout_count++ increment. Auto-dep mode pays more on this dimension too, because every discovered TensorMap edge becomes a fanout_count++ on the producer slot's hot cache line. Manual mode discovers fewer redundant edges → fewer pings.
Evolution directions are discussed in follow-up comments below.
Platform
a2a3 (Ascend 910B/C hardware)
Runtime Variant
tensormap_and_ringbuffer
Summary
Tracking issue for the a2a3
tensormap_and_ringbufferorchestrator. Captures aPTO2_ORCH_PROFILING=1measurement baseline of the per-task submit hot path, side-by-side numbers for the auto-dependency (paged_attention_unroll) and manual-dependency (paged_attention_unroll_manual_scope) variants on the same workload, and the architectural background needed to interpret them.The intent of this issue is characterization, not regression reporting. Concrete evolution directions are posted as separate comments.
Related: #545 (overall runtime perf tracking — scheduler / dispatch / runtime-wrapping side), #849, #902.
Git Commit ID
60742ff
CANN Version
9.0.0 (V100R001C10SPC001B250)
Driver Version
26.0.rc1 (ascendhal_version 7.35.23)
Host Platform
Linux (aarch64)
Reproduction
Both tests use the same
Case1parameters (batch=256, num_heads=16, kv_head_num=1, head_dim=128, block_size=128, context_len=8192, max_model_len=32768, dtype=bfloat16,aicpu_thread_num=4,block_dim=24).Additional Context
1. What the orchestrator does
Single AICPU thread (Thread 3 of 4 on a2a3). Per
runtime/pto_orchestrator.h:11-26: executes the user orchestration function, allocates intermediate buffers from the GM heap (heap ring), submits tasks viaPTO2OrchestratorState::submit_task(real work),submit_dummy_task(barrier), oralloc_tensors(inline-complete buffer carve-out), builds the dependency graph through TensorMap orexplicit_deps, and manages scopes viaPTO2_SCOPE. It is not an executor — it writes records into shared memory and pushes opaque slot-state pointers onto a single SPSC wiring queue; the scheduler threads do all fanout wiring, list maintenance, and watermark advancement.2. Two components: runtime interface and user code
The runtime is split into a function-pointer ops table and the user
.sothat calls it through that table. There is no link dependency: AICPU Thread 3 dumps the orchestration.soto a temp file,dlopens it, looks upaicpu_orchestration_configandaicpu_orchestration_entry, the runtime populatesrt->ops, and the user code calls back viacurrent_runtime()->ops->submit_task(etc.).src/a2a3/runtime/tensormap_and_ringbuffer/orchestration/pto_orchestration_api.h— opaquestruct PTO2Runtime { const PTO2RuntimeOps *ops; PTO2ScopeMode pending_scope_mode; }, thePTO2RuntimeOpsfunction table (submit_task,scope_begin/end,alloc_tensors,submit_dummy_task, logging,set_tensor_data/get_tensor_data, fatal handling), inlinert_submit_*_taskwrappers, theArgbuilder, andPTO2_SCOPE(...)RAII.examples/.../paged_attention_orch.cppexports exactlyaicpu_orchestration_entry(and_config). Includes onlypto_orchestration_api.h. Zero runtime symbol dependencies.3. Six phases of one task submission
All happen inside
submit_task_common(runtime/pto_orchestrator.cpp:507-688), instrumented byCYCLE_COUNT_LAPcalls:g_orch_alloc_cycle) — task ring slot allocation, heap ring packed-output allocation,prefetch_payload,bind_buffers,task_state.store(PENDING),scope_tasks_push. Blocks on back-pressure here.g_orch_sync_cycle) — acquire-loadfc.last_task_alive, possiblycleanup_retiredeveryPTO2_TENSORMAP_CLEANUP_INTERVAL=64retired tasks.g_orch_lookup_cycle) — explicit_deps loop (retire-skip + fanin append) andcompute_task_fanin(pto_dep_compute.h:81-129): per non-OUTPUT tensor, Step A creator retention fromtensor->owner_task_id, then Step Btensor_map.lookup— hash-bucket walk +check_overlapcascade (L1 byte-range, L2 hyper-rectangle, L3 conservative-OTHER). INOUT+COVERED removes the entry. Inline fanin builder caps atPTO2_FANIN_INLINE_CAP, spills to fanin_pool above that.g_orch_insert_cycle) —register_task_outputs(pto_dep_compute.h:140-154): for each INOUT and OUTPUT_EXISTING, allocate entry from free list or bump region, 64-bytecopy_from_tensormemcpy, hash-bucket and per-task chain prepends.g_orch_args_cycle) —task.task_id/kernel_id[3]/packed_buffer_*writes, per-producerfanout_count++, fanin metadata into payload, andpayload.init(args, result, alloc_result, layout)— the actual tensor + scalar deep copy into the GM payload that AICore will read.g_orch_fanin_cycle) — single SPSC push of the slot-state pointer tosched->wiring.queue. The actual fanout wiring (lock + dep_pool prepend + early-finished check + ready-queue push) is deferred to scheduler thread 0'sdrain_wiring_queue(scheduler/pto_scheduler.h:671-711).scope_endcost is tracked separately (g_orch_scope_end_cycle) — iterates scope tasks and callsscheduler->on_scope_endto release the +1 scope reference on each producer'sfanout_count.Raw device-log excerpts on the 1280-task Case1 workload (verbatim, paths anonymized):
4. TensorMap cost and sequentiality
The orchestrator is one thread, so steps 2/3/4 run strictly in order with no pipelining: N tasks back-to-back cost N×(sync + lookup + insert). The TensorMap is private to the orchestrator (no atomics), so there's no contention — but also no parallelism. Auto-dep mode runs
compute_task_faninandregister_task_outputs; manual-dep mode short-circuits both (pto_dep_compute.h:84-86and:142-144). That's why the manual run showsinserts=0andlookups=256(only one trivial lookup per submit, no chain walked, no overlap check).Side-by-side per-phase breakdown on the same 1280-task Case1 workload, same commit, both runs on a free a2a3 device under
task-submitexclusive lock:total(sum of phases)avg/tasktask+heap_allocsync_tensormaplookup+deptensormap_insparam_copyfanin+readyTensorMap-internal stats:
Observations:
param_copystays the largest single phase in both runs (36-52%) — the unavoidable Tensor + scalar deep copy into the GM payload, tensormap-independent.lookup+dep+tensormap_ins) from a combined 151.24 us (23.4%) to 22.88 us (4.7%), a 128.36 us / 19% absolute drop ontotal.sync_tensormapdoes not go to zero in manual mode because it is unconditional (pto_orchestrator.cpp:574-578) — it still loadslast_task_aliveand may runcleanup_retiredeven with zero inserts.fanin+readydrops 47% — orch is pushing fewer slot states through the SPSC wiring queue because explicit deps don't synthesize redundant edges.task+heap_allocis essentially flat — the ring buffers are uncontested at this 1280-task / 16K-window scale.lookupalways pays the hash + cache-line fetch + iterate cost.5. Orch ↔ scheduler interaction factors
Two distinct cost regimes for orch↔sched data sharing on the hot path:
PTO2TaskDescriptor,PTO2TaskPayload, and the per-slotPTO2TaskSlotStatepayload/task pointers are written once by the orchestrator at step 5, read once by the scheduler at dispatch (and again by AICore at execute). No coherence ping-pong: orch dirties, sched reads. Per-task payload is bounded (≤16 tensors + ≤16 scalars), and active payload working set fits in L2 by design —task_window_sizeis small, default 16384/ring, often as small as 16. Cost: bandwidth only.PTO2RingFlowControl(pto_shared_memory.h:54-76) —current_task_indexandlast_task_aliveare on separate 64B-aligned lines on purpose, so each is single-writer.PTO2TaskSlotState(pto_runtime2_types.h:316-398) — single 64B line carrying both orch-managed (task_state=PENDING,bind_buffers,active_mask,fanout_countincrements on producers) and sched-managed (task_state=COMPLETED/CONSUMED,fanout_refcount,fanin_refcount,fanin_count) fields. Pings on every fan-out edge.sched->wiring.queueslot lines — orch pushes (step 6), sched batch-pops (drain_wiring_queue). One ping per submit.Factor B is what scales with fan-out density. The recent deferred-wiring redesign (orch no longer takes
fanout_lockper producer, no longer allocatesdep_pool) was a targeted cut on this; the remaining orch-side coherence work is the per-producerfanout_count++increment. Auto-dep mode pays more on this dimension too, because every discovered TensorMap edge becomes a fanout_count++ on the producer slot's hot cache line. Manual mode discovers fewer redundant edges → fewer pings.Evolution directions are discussed in follow-up comments below.