PMU Profiling — Per-task AICore Hardware Counters¶
1. Background & Motivation¶
AICore performance issues — pipeline stalls, memory-bandwidth shortfalls, cache misses — are invisible at the runtime layer. The hardware exposes a Performance Monitoring Unit (PMU) per core with a small bank of counters that can be programmed to track these microarchitectural events, but reading them only at run start/end conflates every task into one number.
PMU profiling samples those counters once per runtime task, so each
row in the output corresponds to a single kernel invocation. That
makes it possible to attribute a hot counter (e.g. high mte2_busy
or low cube_busy) to a specific func_id instead of "the run".
2. Overview¶
- One row per task. Counters are sampled at task completion, not as a post-run aggregate.
- Selectable event group. A single
--enable-pmu Nflag picks which counter group is active for the run (PIPE_UTILIZATION,MEMORY,L2_CACHE, …). - CSV output, fixed schema. A
pmu.csvlands under the per-task output prefix; the column order is the same on both architectures for tooling parity. - Cross-architecture. Same Python entry point, same CSV format on
a2a3anda5. Wired through bothhost_build_graphandtensormap_and_ringbufferruntimes.
Enable in one line:
3. How to Use¶
3.1 Enable PMU¶
Bare flag selects the default PIPE_UTILIZATION event group:
# Standalone runner
python tests/st/<case>/test_<name>.py -p a2a3 -d 0 --enable-pmu
python tests/st/<case>/test_<name>.py -p a5 -d 0 --enable-pmu
# pytest
pytest tests/st/<case> --platform a2a3 -d 0 --enable-pmu
pytest tests/st/<case> --platform a5 -d 0 --enable-pmu
--enable-pmu alone is equivalent to --enable-pmu 2. Pass an explicit
value (see §3.3) to switch event groups:
The SIMPLER_PMU_EVENT_TYPE environment variable overrides the CLI
event type when set:
--rounds > 1 disables PMU collection in the test harness so warm-up
rounds are not double-counted.
3.2 Output¶
The PMU artifact is a CSV file under the per-task output prefix
(CallConfig::output_prefix, set by scene_test.py::_build_output_prefix
to outputs/<ClassName>_<case>_<YYYYMMDD_HHMMSS>/ for SceneTest runs):
The filename is fixed (no per-file timestamp) — the directory is the per-task uniqueness boundary.
Common columns (in order, identical across architectures):
| Column | Meaning |
|---|---|
thread_id |
AICPU scheduler thread that drives this core |
core_id |
Logical AICore id in the runtime |
task_id |
Runtime task id, printed as hex |
func_id |
Kernel function id |
core_type |
0 = AIC, 1 = AIV |
pmu_total_cycles |
64-bit PMU_CNT_TOTAL snapshot |
| event-specific counters | Counter columns selected by the event type |
event_type |
Numeric event type used for the run |
The number of counter columns varies by event type — each event group
populates a different subset of the hardware counter slots, and the
CSV lists only the slots that have a defined name. Use the
event_type column to discover which counters are present in a given
file.
Example row (PIPE_UTILIZATION on a2a3, abbreviated):
thread_id,core_id,task_id,func_id,core_type,pmu_total_cycles,
vec_busy_cycles,cube_busy_cycles,scalar_busy_cycles,
mte1_busy_cycles,mte2_busy_cycles,mte3_busy_cycles,
icache_miss,icache_req,event_type
2,5,0x0000000200000a00,0,1,18432,
1024,0,512,0,256,128,
3,448,2
Read: func_id=0 ran on AIV core 5 (driven by AICPU thread 2),
took ~18 K total cycles, vector pipe was busy for 1024 cycles, cube
was idle, MTE2 (load) ran 256 cycles, etc.
3.3 Event types¶
| Value | Event Type | Example Counters |
|---|---|---|
1 |
ARITHMETIC_UTILIZATION |
cube/vector execution counters |
2 |
PIPE_UTILIZATION |
vector, cube, scalar, MTE busy cycles |
4 |
MEMORY |
UB / L1 / L2 / main memory requests |
5 |
MEMORY_L0 |
L0A / L0B / L0C requests |
6 |
RESOURCE_CONFLICT |
bank and vector resource stalls |
7 |
MEMORY_UB |
UB and memory bandwidth counters |
8 |
L2_CACHE |
L2 cache hit / miss / allocation counters |
Invalid nonzero values fall back to PIPE_UTILIZATION.
Each architecture programs its own per-counter event-code space
(pmu_resolve_event_config_a2a3 / pmu_resolve_event_config_a5), so
the specific counter column names differ per event group per
architecture. The two PIPE_UTILIZATION rosters as a concrete example:
a2a3 (DAV_2201, 8 slots):
vec_busy_cycles, cube_busy_cycles, scalar_busy_cycles,
mte1_busy_cycles, mte2_busy_cycles, mte3_busy_cycles,
icache_miss, icache_req
a5 (DAV_3510, 10 slots):
pmu_idc_aic_vec_busy_o, cube_instr_busy, scalar_instr_busy,
mte1_instr_busy, mte2_instr_busy, mte3_instr_busy,
icache_req, icache_miss, pmu_fix_instr_busy
4. Capabilities¶
What you can read out of pmu.csv:
- Per-task pipeline utilization (
PIPE_UTILIZATION) — how busy vector / cube / scalar / MTE pipes were during each kernel. - Per-task arithmetic mix (
ARITHMETIC_UTILIZATION) — fp16 / int8 / fp32 instruction counts on cube and vector pipes. - Memory traffic (
MEMORY,MEMORY_L0,MEMORY_UB,L2_CACHE) — read/write request counts and cache hit/miss tallies at each level of the memory hierarchy. - Resource contention (
RESOURCE_CONFLICT) — bank-conflict and vector-resource-stall cycle counts. - Per-task total cycles (
pmu_total_cycles, present in every event group).
For a single run, only one event group is active. Iterate the run
under different --enable-pmu N values to cover other counter groups.
5. Design Highlights¶
Three layers cooperate; the split is the same across architectures:
- Host owns user entry, event-type selection, allocation, and CSV
export. Publishes a single device pointer through
kernel_args.pmu_data_basethat points at the architecture's PMU shared region. - AICPU programs the PMU event group at init, starts/stops the
counters via
PMU_CTRL_0/1, observes per-task FIN, and commits onePmuRecordper task. The owning AICPU scheduler thread index is carried out of band on the ready-queue entry (its per-thread queue index inPmuDataHeader::queues[thread][...]) and threaded through to the CSV row asthread_id. - AICore brackets the counting window around the kernel body via
CTRLSPR bit 0.
The per-task counter readout and the host↔device buffer transport are architecture-specific. Sections 5.2 and 5.3 describe each architecture end-to-end; §5.4 is a side-by-side comparison.
5.1 Common interfaces¶
kernel_args.pmu_data_base is the single device-side handle host
publishes for the run. Its target struct is PmuDataHeader on both
architectures; in both cases it carries:
num_cores— number of AICore instances in useevent_type—PmuEventTypevalue the host wrote at init time
AICPU reads it on init to find per-core state. On every task FIN it
commits a PmuRecord and increments PmuBufferState::total_record_count.
On the drop path it increments PmuBufferState::dropped_record_count
instead. a5 also tracks PmuBufferState::mismatch_record_count for
records lost to ring-slot task_id mismatch (a hard invariant
violation, distinct from capacity drops). Host uses these counters at
finalize for the cross-check:
collected_on_host + dropped + mismatch == total (a5, 3 buckets)
collected_on_host + dropped == total (a2a3, 2 buckets)
5.2 a2a3 — shared-memory streaming (DAV_2201, 8 counters)¶
AICPU reads the 8 PMU counters via MMIO (read_reg(reg_base, PMU_CNTi))
directly into a PmuRecord on every task FIN. Buffers rotate through
an SPSC free queue per core; full buffers flow through a per-thread
ready queue to host drain/refill shards. Drain refills free queues from
shard-local recycled lanes; collector shards stream records to CSV during
execution, and the replenish thread routes done buffers to lanes below their
recycled watermarks before allocating any remaining top-up. It never writes
device free queues. PMU has no init-seeded recycled surplus by default, so the
runtime watermark is only a minimal reserve batch rather than a
core-count-scaled extra allocation target.
HOST DEVICE
┌──────────────────────────┐ ┌──────────────────────────┐
│ PmuCollector │ │ AICPU thread │
│ │ │ │
│ init() │ alloc + │ pmu_aicpu_init() │
│ rtMalloc + halRegister │──register────>│ read PmuDataHeader │
│ pre-fill free queues │ │ pop initial buffer │
│ │ │ per-core │
│ │ │ │
│ start(tf) │ │ per-task FIN: │
│ ┌────────────────────┐ │ │ read 8 PMU_CNTs+TOTAL │
│ │ drain/refill shard │ │ │ into records[count] │
│ │ + replenish thread │ │ SPSC ready │ if buffer full: │
│ │ poll ready queue │<┼──queues──────<│ push ready entry, │
│ │ refill freeQ │─┼──free queue──>│ pop next buffer │
│ └────────────────────┘ │ │ │
│ ┌────────────────────┐ │ shared mem │ pmu_aicpu_flush(): │
│ │ collector shard │ │ mapping │ push remaining full │
│ │ read records via │<┼──────────────<│ buffers to ready_q │
│ │ host mapping │ │ │ │
│ │ append to shard │ │ │ │
│ │ temp CSV │ │ │ │
│ └────────────────────┘ │ └──────────────────────────┘
│ │
│ stop() │
│ join mgmt → collectors │
│ reconcile_counters() │
│ finalize() │
└──────────────────────────┘
Device memory layout (pmu_data_base →):
PmuDataHeader (host init, AICPU/host R/W)
├── queues [MAX_AICPU_THREADS][READYQUEUE_SIZE]
├── queue_heads / queue_tails (per-thread)
├── num_cores
└── event_type
PmuBufferState[num_cores] (per-core state)
├── free_queue {buffer_ptrs[SLOT_COUNT], head, tail}
├── current_buf_ptr (AICPU active buffer)
├── current_buf_seq
├── dropped_record_count
└── total_record_count
PmuBuffer pool (rotated) (BUFFERS_PER_CORE per core)
└── PmuRecord records[RECORDS_PER_BUFFER] + count
Lifecycle (device_runner.cpp):
init_pmu()
pmu_collector_.init(num_aicore, num_threads, csv_path, event_type, ...)
kernel_args_.args.pmu_data_base = pmu_collector_.get_pmu_shm_device_ptr()
start(tf) ← spawn split mgmt threads (drain AICPU ready
queues and refill free queues from
recycled lanes; replenish drains done
buffers into recycled lanes)
+ collector shards (drain host hand-off,
append to shard-local temp CSV files)
launch AICPU / AICore
rtStreamSynchronize ← wait for kernel completion
stop() ← join mgmt/replenish → join collectors
reconcile_counters() ← merge shard files into pmu.csv, then assert
collected + dropped == total;
any non-empty current_buf_ptr is a
flush bug, logged as ERROR
finalize(unregister, free)
PmuCollector
inherits from
profiling_common::ProfilerBase<PmuCollector, PmuModule>:
the base class owns split mgmt threads, collector shards, and the
BufferPoolManager<PmuModule> they share. PmuCollector only supplies
the PMU-specific pieces — the PmuModule trait that describes the
shared-memory layout, an init() that allocates and pre-fills the free
queues, an on_buffer_collected() callback that appends records to a
shard-local temporary CSV, and reconcile_counters() / finalize() that
merge those files into pmu.csv. A failed merge preserves the temporary
files and does not count their rows as successfully exported. Successful
reconciliation removes the temporary files. An abrupt process exit can leave
them in the CSV directory; the next start() for the same output path removes
those stale run-scoped files before collector threads launch. The
mgmt/collector threading, buffer pooling, and Module trait pattern are shared
with ArgsDump and L2Swimlane — see
profiling-framework.md for the framework reference.
5.3 a5 — same framework, host-shadow transport (DAV_3510, 10 counters)¶
AICore reads the 10 PMU counters via the ld_dev MMIO load intrinsic
into a per-core dual-issue staging slot indexed by reg_task_id & 1.
AICPU, on observing FIN, validates the slot's recorded task_id
against the register token, copies the record into
PmuBuffer::records[count], fills func_id / core_type, and
advances count. When a buffer fills up, AICPU switches to a new
buffer via the SPSC free queue / ready queue protocol (identical to
a2a3). At shutdown, AICPU flushes any partially-filled buffers via
pmu_aicpu_flush_buffers().
a5's PmuCollector derives from
ProfilerBase<PmuCollector, PmuModule> and uses the same framework
abstractions as a2a3, including the same split mgmt + collector shard
shape (kMaxCollectorThreads = PLATFORM_MAX_AICPU_THREADS, i.e. 7 on
a5 vs 4 on a2a3, capping the shard arrays; the live drain/collector
count is min(aicpu_thread_num, kMaxCollectorThreads)). The
behavioral deviation from §5.2 is the transport channel: a5 has no
halHostRegister, so
each device buffer is paired with a host-shadow malloc() and the
mgmt loop synchronizes the two via profiling_copy.h (rtMemcpy
onboard, memcpy in sim). MemoryOps therefore carries five
callbacks (alloc / reg / free_ / copy_to_device /
copy_from_device); the mgmt loop mirrors the entire shm region
(PmuDataHeader + per-core PmuBufferState) device → host at the
top of every tick, then pushes back only the fields host modified
(advanced queue_heads[q], refilled free_queue.tail and
buffer_ptrs[slot]) via BufferPoolManager::write_range_to_device.
The bulk mirror_shm_to_device is not called from the mgmt
loop: it would race with AICPU writes to device-only fields
(current_buf_ptr, total/dropped counters, queue_tails,
free_queue.head) and roll them back. Each popped PmuBuffer is
still pulled on demand inside
ProfilerAlgorithms::process_entry.
HOST DEVICE
┌──────────────────────────┐ ┌──────────────────────────┐
│ PmuCollector │ │ AICore │
│ : ProfilerBase<...> │ │ │
│ │ │ │
│ init() │ alloc + reg │ per-task end: │
│ rtMalloc data region │──+ shadow────>│ ld_dev 10 PMU_CNTs + │
│ per-core PmuBuffers │ memset 0 │ PMU_CNT_TOTAL │
│ register_mapping(s) │ + push 0s │ write into │
│ build PmuDataHeader │ │ dual_issue_slots[ │
│ │ │ reg_task_id & 1] │
│ start(thread_factory) │ │ │
│ mgmt_thread starts │ │ AICPU thread │
│ poll_thread starts │ │ on FIN: │
│ │ │ match slot's task_id │
│ mgmt every 10us tick: │ │ vs reg_task_id │
│ copy_from_device(shm) │<──memcpy─────<│ copy into │
│ for each ready entry: │ │ records[count] │
│ copy buf from device │<──memcpy─────<│ fill func_id/core_type │
│ resolve host ptr │ │ ++count │
│ push to host ready_q │ │ if buffer full: │
│ advance queue_heads, │ │ push ready entry, │
│ refill free_queues │ │ pop next from free_q │
│ write_range_to_device │──memcpy──────>│ │
│ for each modified │ │ pmu_aicpu_flush(): │
│ field │ │ push remaining full │
│ │ │ buffers to ready_q │
│ poll thread: │ │ │
│ wait_pop_ready │ │ │
│ on_buffer_collected → │ │ │
│ write shard temp CSV │ │ │
│ notify_copy_done │ │ │
│ │ │ │
│ rtStreamSynchronize │ │ │
│ stop() │ │ │
│ join mgmt + poll │ │ │
│ reconcile_counters() │ │ │
│ sanity-check leftovers │ │ │
│ + cross-check │ │ │
│ finalize(free) │ │ │
└──────────────────────────┘ └──────────────────────────┘
Device memory layout:
[PmuDataHeader] (kernel_args.pmu_data_base)
├── queues [MAX_AICPU_THREADS][READYQUEUE_SIZE]
├── queue_heads / queue_tails (per-thread)
├── num_cores
└── event_type
[PmuBufferState[num_cores]] (per-core state)
├── free_queue {buffer_ptrs[SLOT_COUNT], head, tail}
├── current_buf_ptr (AICPU active buffer)
├── aicore_ring_ptr (stable PmuAicoreRing*, host writes once)
├── current_buf_seq
├── total_record_count
├── dropped_record_count
└── mismatch_record_count (ring slot / task_id invariant violations)
PmuAicoreRing[num_cores] (stable AICore staging, never rotated)
└── PmuRecord dual_issue_slots[PLATFORM_PMU_AICORE_RING_SIZE]
PmuBuffer pool (rotated) (BUFFERS_PER_CORE per core)
└── PmuRecord records[RECORDS_PER_BUFFER] + count
halHostRegister is not supported on DAV_3510, so the a5 collector
maintains a paired host-shadow malloc() per device buffer and
synchronizes via rtMemcpy (onboard) / memcpy (sim). The framework
copy hooks profiling_copy_to_device / profiling_copy_from_device
(in profiling_copy.h)
abstract this difference.
Each AICore worker resolves its PMU MMIO base at kernel entry from
KernelArgs::regs[get_physical_core_id()] (the per-physical-core
register-base table the host already fills for AICPU) and reads its
PmuAicoreRing from KernelArgs::aicore_pmu_ring_addrs[block_idx]
(filled by the host in PmuCollector::init). Both addresses are
forwarded by KERNEL_ENTRY into platform-owned per-core slots
(aicore_profiling_state.h);
the runtime Handshake carries no profiling fields. Because the
resolved reg base is valid from Phase 1 onward (no AICPU-side init
dependency), aicore_execute caches it once after Phase 3 alongside
the rings rather than re-reading per record.
Lifecycle (device_runner.cpp):
init_pmu()
pmu_collector_.init(num_aicore, num_threads, csv_path, event_type, ...)
kernel_args_.args.pmu_data_base = pmu_collector_.get_pmu_shm_device_ptr()
kernel_args_.args.aicore_pmu_ring_addrs = pmu_collector_.get_aicore_ring_addrs_device_ptr()
→ AICPU pmu_aicpu_init() resolves
per-core PMU MMIO bases from
regs[physical_core_ids[i]] and
caches state->aicore_ring_ptr.
AICore separately resolves its
own base at kernel entry from
regs[get_physical_core_id()].
pmu_collector_.start(thread_factory) ← mgmt + poll threads
launch AICPU / AICore
rtStreamSynchronize
pmu_collector_.stop() ← join mgmt + poll, drain final batch
pmu_collector_.reconcile_counters() ← merge shard files, then verify
collected + dropped + mismatch == device_total
pmu_collector_.finalize(free)
Slot match key vs logical task_id. pmu_aicpu_complete_record
takes both a 32-bit reg_task_id (the value AICore read from
DATA_MAIN_BASE and stored in slot->task_id) and a 64-bit logical
task_id written into the record itself. Runtimes whose logical id
encodes more than 32 bits (e.g. tensormap_and_ringbuffer's
(ring_id<<32)|local_id) carry both — slot match must use the
register token, otherwise the slot will never validate.
The two dual-issue slots exist because dispatch can have up to two
tasks in flight on a single AICore. Parity on reg_task_id & 1 keeps
adjacent dispatches from colliding (the runtime's dispatch_seq++
guarantees neighboring register tokens differ by 1 → different slots).
PmuCollector on
a5 inherits the same CRTP base
(profiling_common::ProfilerBase)
as a2a3 and parameterizes
BufferPoolManager
with PmuModule. The only a5-specific glue is the 5-callback
MemoryOps and the per-tick shm mirror.
a5's per-thread AICPU flush (pmu_aicpu_flush_buffers) is the only
data path on the records side — host never reads from
current_buf_ptr to recover records. reconcile_counters is purely
passive: it logs an error if any current_buf_ptr is non-zero with
a non-empty buffer (a device-flush bug), then runs the three-bucket
cross-check collected + dropped + mismatch == device_total against
device-side counters.
5.4 a2a3 vs a5 at a glance¶
| Aspect | a2a3 | a5 |
|---|---|---|
| HW counter slots | 8 (DAV_2201) | 10 (DAV_3510) |
| Counter readout | AICPU MMIO read_reg |
AICore MMIO ld_dev |
| Per-core staging | direct write into records[count] |
dual-issue slots, AICPU commits on FIN |
| Buffer model | rotating pool (free + ready queues, SPSC protocol) | identical |
| Host threads | split mgmt + collector shards, writes shard-local temp files during execution and merges at reconcile | same split mgmt + collector shards (7 = PLATFORM_MAX_AICPU_THREADS vs a2a3's 4) |
| Host-class shape | ProfilerBase<PmuCollector, PmuModule> |
identical |
| Host transport | halHostRegister shared memory |
host-shadow malloc + per-tick rtMemcpy/memcpy |
MemoryOps callbacks |
3 (alloc, reg, free_) |
5 (+ copy_to_device, copy_from_device) |
reconcile_counters |
passive cross-check (collected + dropped == device_total) | passive cross-check with mismatch bucket (collected + dropped + mismatch == device_total); leftover non-empty current_buf_ptr logged as a device flush bug |
| Lifecycle | init → start → stop → reconcile_counters → finalize |
identical |
6. Overhead¶
PMU profiling is opt-in and zero-overhead when disabled — without
--enable-pmu neither host nor device allocates PMU storage and the
counter-read code paths are skipped.
When enabled, the dominant per-task overhead is the MMIO counter read
(8 reads on a2a3, 10 on a5) plus a single record copy. On both
architectures, shard-local streaming keeps host-side work off the critical
path without serializing collectors on one output stream. Reconciliation
merges the bounded temporary files into the final CSV after collection stops.
Both a2a3 and a5 use split mgmt plus collector shards (a5 with 7 shards,
a2a3 with 4). a5's copy hooks add rtMemcpy round-trips that a2a3's
shared memory avoids, but these overlap with device execution.
For meaningful per-task numbers on a2a3 the runtime collapses to
single-issue dispatch automatically whenever --enable-pmu is set (see
§7.1) — this serialization itself costs throughput, so PMU-on
measurements are not comparable to PMU-off baselines.
7. Limitations¶
7.1 a2a3¶
PMU collection assumes each logical AICore has at most one in-flight
task. The default dual-issue dispatch preloads a pending task while
another task is still running on the same core, so per-core PMU
registers can carry overlapping task windows. To keep counters scoped
to a single task, --enable-pmu automatically collapses dispatch to
single-issue at runtime — both host_build_graph and
tensormap_and_ringbuffer runtimes branch on is_pmu_enabled() in
their dispatch path. No separate flag or rebuild is required.
Notes on this constraint:
- PMU-on runs serialize dispatch per core, so throughput is lower than PMU-off baselines. The two are not directly comparable.
a2a3simexercises the export pipeline; counter values come from the simulation backend, not real hardware, so they are not suitable for performance analysis.
7.2 a5¶
a5simexercises the export pipeline; the simulated counter register block does not model AICore execution, so counter values are 0. The CSV still carries one row per task with a zero counter tuple — useful for validating the end-to-end data flow.- The per-core on-device
PmuBuffercapacity is controlled byPLATFORM_PMU_RECORDS_PER_BUFFER(default 512). When full, AICPU switches to a new buffer via the free queue. If no free buffer is available, records are dropped. IncreasePLATFORM_PMU_BUFFERS_PER_CORE(default 4) in platform_config.h if your workload produces bursts that exhaust the buffer pool. -
A non-zero
diffin the host'srecord count mismatchwarning means AICPU attempted to commitdiffrecords whose dual-issue slot still carried an oldertask_id. With AICore's slot-write order (counters → pmu_total_cycles → store barrier → task_id → dcci → dsb → write FIN to COND),diffshould always be zero on DAV_3510. A persistent non-zerodiffis a sharp diagnostic — find the regression rather than tuningPLATFORM_PMU_RECORDS_PER_BUFFER. Common causes: -
The
reg_task_idproducer/consumer drifted out of sync (AICPU uses a different task-id encoding than AICore wrote into the slot). - AICPU calls
pmu_aicpu_complete_recordfor a task AICore never executed (e.g. an AICPU-only task path; AICore never wrote that slot, sotask_idstays stale). - AICore's
dcci/dsbordering around the slot write was rearranged, or a barrier was weakened from fulldsbto a store-only flavor. - The hardware target's
dcci(..., CACHELINE_OUT)semantics differ (e.g. non-DAV_3510 ports) and no longer guarantee HBM writeback before the followingdsb.
8. FAQ / Debug Guide¶
No pmu.csv produced. Check that --enable-pmu was passed (or
SIMPLER_PMU_EVENT_TYPE was set with the flag). Verify
<output_prefix> exists in the run log; if --rounds > 1, PMU
collection is suppressed by the harness.
All counter columns are zero. Either the platform is a2a3sim /
a5sim (counter registers are not modelled), or the active event
group does not populate the columns shown — check the event_type
column and the per-architecture event table in §3.3.
Counter values look polluted on a2a3. Dual-issue dispatch is
overlapping tasks on the same core. --enable-pmu should already
collapse dispatch to single-issue at runtime (§7.1); if pollution
persists, verify that is_pmu_enabled() returns true on every AICPU
thread and that the dispatch loop branch in scheduler_dispatch.cpp
and aicpu_executor.cpp hasn't been bypassed.
record count mismatch (... diff=M) on a5. Slot-mismatch loss —
this should be 0 on DAV_3510. Treat as a regression: see §7.2 for the
four common causes and check the reg_task_id / barrier / dcci
chain rather than tuning buffer sizes.
current_buf_ptr non-empty at finalize on a2a3. The host logs
this as ERROR and does not recover. It indicates AICPU did not flush
its active PMU buffer at run end. Check pmu_aicpu_flush_buffers is
called for every AICPU thread, and that the per-thread core list
covers every core that produced records.
Dropped records on a2a3. PmuBufferState::dropped_record_count
nonzero means the AICPU could not get a free buffer in time
(free_queue empty). Increase PLATFORM_PMU_BUFFERS_PER_CORE so the
host drain/replenish path has more buffer headroom.
Dropped records on a5. PmuBufferState::dropped_record_count
nonzero means the AICPU could not get a free buffer in time
(free_queue empty). Increase PLATFORM_PMU_BUFFERS_PER_CORE so the
host drain/replenish path has more buffer headroom.
9. Related docs¶
- profiling-framework.md — shared host-side collector framework.
- chip-level-arch.md — host / AICPU / AICore program boundaries the PMU path spans.
- task-flow.md — where AICPU dispatch and completion sit in the per-task state machine.