diff --git a/.claude/skills/l0-swimlane/SKILL.md b/.claude/skills/l0-swimlane/SKILL.md index 5e18e1c18..cd4fcc617 100644 --- a/.claude/skills/l0-swimlane/SKILL.md +++ b/.claude/skills/l0-swimlane/SKILL.md @@ -1,6 +1,6 @@ --- name: l0-swimlane -description: Produce an L0 (intra-core AICore pipeline) swimlane for one task — a single kernel or a mix — via the dump-driven `simpler_setup.tools.l0_swimlane` tool. Use when the user asks to "run/produce an l0 swimlane", "trace a task's intra-core pipeline", profile why one AICore task is slow inside the core(s), or needs help choosing the tool's manual flags (`--func-id`, `--set-arg`, `--spmd-block-num`, `--case`). The tool captures real per-task args from an args dump and auto-generates the `msprof op simulator` replay — no hand-authored workspace. For a hand-authored single-`kernel_entry` replay use [insight-trace](../insight-trace/SKILL.md); for cross-task / scheduler / dependency timing use the L2 swimlane. +description: Produce an L0 (intra-core AICore pipeline) swimlane for one task — a single kernel or a mix — via the dump-driven `simpler_setup.tools.l0_swimlane` tool. Use when the user asks to "run/produce an l0 swimlane", "trace a task's intra-core pipeline", profile why one AICore task is slow inside the core(s), or needs help choosing the tool's manual flags (`--func-id`, `--restore-arg`, `--set-arg`, `--spmd-block-num`, `--case`). The tool captures real per-task args from an args dump and auto-generates the `msprof op simulator` replay — no hand-authored workspace. For a hand-authored single-`kernel_entry` replay use [insight-trace](../insight-trace/SKILL.md); for cross-task / scheduler / dependency timing use the L2 swimlane. --- # L0 Swimlane — Intra-core Pipeline Trace for a Task @@ -10,8 +10,9 @@ reconstructs them, generates a combined `msprof op simulator` replay of the **whole task** (a mix runs AIC + AIV0 + AIV1 in one op), and exports an Insight `trace.json` whose lanes are the cluster's pipes. Full reference: [docs/dfx/l0-swimlane-profiling.md](../../../docs/dfx/l0-swimlane-profiling.md). -This skill is the **operating procedure** — above all the one genuinely -manual decision: the slot/value for `--set-arg`. +This skill is the **operating procedure** — above all the manual decisions: +whether a structured control tensor needs `--restore-arg`, and the +slot/value for any `--set-arg`. ## When to use @@ -42,6 +43,15 @@ Onboard `a2a3` instead of `a2a3sim`: run runs on the locked device). The five internal steps and all flags are in doc §3.2 / §3.3. +**Payload-restoration scope:** `--restore-arg` is supported only with +`--platform a2a3sim` or `a2a3`. On `a5sim` / `a5`, tensor payload bytes from +args dump are not trustworthy because of +[#1560](https://github.com/hw-native-sys/simpler/issues/1560), so do not use +them as tensor truth or pass `--restore-arg`. Metadata-only and zero-filled +A5 L0 replays remain usable. A5 still writes the selected level-3 `args.bin`, +but an L0 trace restored from it is untrustworthy until #1560 is fixed; this +limitation does not make the whole L0 tool a2a3-only. + ## Choosing the manual flags (the hard part) ### `--func-id` — the task's member set @@ -53,6 +63,25 @@ lanes SPMD mix the dump records a duplicate (`[0,1,1]`), so pass `--func-id 0,1` (`set([0,1,1]) == {0,1}`). Wrong set → the tool lists the func_id shapes present in the dump; pick one of those. +### `--restore-arg SLOT` — real structured control data + +This is currently an **a2a3/a2a3sim-only** capability. Do not use A5 payload +for restoration while +[#1560](https://github.com/hw-native-sys/simpler/issues/1560) is open. + +Use this repeatable flag when a tensor contains non-uniform runtime-built +state that changes control flow or addresses, such as a tiling struct. +The fresh capture stays at `--dump-args 3`, but adds the internal SceneTest +selector `--dump-args-payload-func-id ` plus one +`--dump-args-payload-slot ` per restore. It restores those selected +`before_dispatch` bytes and leaves all unselected tensor payloads zero. A +reused `--dump-json` must have its sibling `args.bin` and the requested +record payload. + +Do not use it for weights, activations, or KV pools. The selective level-3 +capture avoids dumping unrelated task payloads. A restored slot cannot also +use `--set-arg`. + ### `--set-arg SLOT=VALUE` — only when a loop count must shrink First classify where the kernel's loop trip count comes from: @@ -62,6 +91,7 @@ First classify where the kernel's loop trip count comes from: | **Tensor shape** (e.g. `shapes[0] / TILE_ELEMS`) | **No** | shape is the real dump value; changing it distorts. (mixed_example / single-kernel rows need no `--set-arg`.) | | **A scalar arg** (e.g. `n_blocks`) | **Yes** — set the count directly | camodel would run the full loop; shrink to ≥ 3–4 (doc §7.2: floor 3, prefer 4). | | **A control-tensor's content** (e.g. `context_lens`) | **Yes** — fill the buffer | the kernel *derives* the count from the data; fill so the derived count ≈ 4 (need `block_size` to back out the value). Integer dtypes only. | +| **A structured control tensor** (e.g. `FAInferTilingData`) | **No** — use `--restore-arg` on a2a3/a2a3sim | One uniform integer cannot reproduce a struct; restore its captured bytes on the supported platform family. | | **The SPMD `block_num`** | **No** — use `--spmd-block-num` | block_num lives in the synthesised slot-48 context, which `--set-arg` cannot reach. | Then find the slot — it is **per-kernel, never fixed**. Discover it: @@ -86,10 +116,18 @@ Verified examples (slots read from source): ### `--spmd-block-num N` — SPMD grid width `block_num` is written into the synthesised slot-48 `LocalContext`. Default -is 1; pass it to replay an SPMD cohort, or for a kernel that branches or -grid-strides on `block_num` (e.g. set the real hw width `24`). `block_idx` -is always synthesised to `0` (a representative block) and is **not** a flag — -it has no instruction-stream branches (doc §8). +is 1, which models a single-block replay and does not take multi-block +branches such as `block_idx + 1 < block_num`. Keep that default only when the +kernel ignores `block_num`. For a kernel that branches or grid-strides on it, +read the selected task's real logical width from its orchestration and pass it +explicitly. That value may come from `rt_available_cluster_count()`, or it may +be task-specific. For example, the default lowest task-id in +`spmd_multiblock_aiv` uses `--spmd-block-num 4`, while the default lowest task +in `spmd_multiblock_mix` uses `--spmd-block-num 2`. When the real value is +unavailable, a value of at least 2 is only a bounded approximation for +exercising the next-block branch; record that limitation in the analysis. +`block_idx` is always synthesised to `0` (a representative block) and is +**not** a flag — it has no instruction-stream branches (doc §8). ### `--case NAME` — pin a small case on a multi-case test @@ -111,6 +149,35 @@ per-block pipeline stays identical to production (you lose only iteration *count*, which does not change the pipeline shape). A case with different tile shapes traces only itself, not production. +### Oversized CAModel checklist + +On the supported a2a3/a2a3sim path, selective level-3 payload capture only +shrinks `args.bin`; the replay still allocates and zero-initializes every +tensor at its dumped shape. For a large weight or KV tensor: + +1. Run with `--dump-json --no-collect` first and inspect the printed + slot shapes. +2. Use `--case ` to reduce the actual shape-driving extents + (layers, pages, cache rows, or sequence blocks). Lowering runtime `seq_len` + alone does not help when shapes use fixed `MAX_SEQ` or layer constants. +3. Use `--set-arg` only when the remaining cost is a loop count; it does not + resize tensors. On a2a3/a2a3sim, use `--restore-arg` only to preserve + structured control state; it does not make the replay smaller. Do not use + A5 payload restoration while #1560 is open. +4. Keep the default `--msprof-timeout 120` for ordinary cases. For a large, + repetitive workload where a partial pipeline is sufficient, set a shorter + timeout such as `--msprof-timeout 4`. This is the behavior recommended by + the [Ascend CANN official QA/parameter guidance for msProf `--timeout`](https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/82RC1alpha001/devaids/optool/atlasopdev_16_0082.html): + msProf terminates simulation at the limit and parses the data produced so + far. A partial trace can show the observed pipeline segment, but cannot + establish whole-kernel duration or tail completeness. If the target kernel + produced no profiler data before timeout, there may be no `trace.json`. + +If no bounded case exists, make a focused workload-local case with the same +target task, func-id set, argument order, tiling path, and tile geometry but +smaller shape-driving extents. Keep a diagnostic-only case out of the PR unless +it is intended as permanent regression coverage. Full details are in doc §3.6. + ## Self-check after every run A known msprof/camodel export bug can truncate the last loop iteration(s). @@ -123,4 +190,4 @@ raw Insight `trace.json`, for sub-laned per-instruction overlap (doc §3.4). Representative command per task shape (single AIC / single AIV / 1+1 mix / 2-AIV mix / 3-way mix / SPMD single-source / SPMD coop mix / same-AIV-both- -lanes / paged-attn scalar & control-tensor loops / qwen3) is in doc §3.7. +lanes / paged-attn scalar & control-tensor loops) is in doc §3.8. diff --git a/conftest.py b/conftest.py index bd1e03371..4c8c49b7e 100644 --- a/conftest.py +++ b/conftest.py @@ -156,7 +156,22 @@ def pytest_addoption(parser): default=0, help="Dump per-task args at runtime. Level: 0=off, 1=partial (only " "tasks marked via Arg::dump(...), default when given without a value), 2=full (all tasks), " - "3=full_json_only (all tasks, JSON metadata only, no .bin payload).", + "3=full_json_only (all tasks' JSON metadata; no payload unless a selective payload filter is given).", + ) + parser.addoption( + "--dump-args-payload-func-id", + action="store", + default=None, + metavar="ID[,ID...]", + help="With --dump-args 3, capture tensor payload only for tasks whose active func-id set matches.", + ) + parser.addoption( + "--dump-args-payload-slot", + action="append", + type=int, + default=[], + metavar="SLOT", + help="With --dump-args 3, capture this tensor args[] slot for the selected task. Repeatable.", ) parser.addoption( "--enable-dep-gen", diff --git a/docs/dfx/args-dump.md b/docs/dfx/args-dump.md index 0751dc245..3a4aa15e5 100644 --- a/docs/dfx/args-dump.md +++ b/docs/dfx/args-dump.md @@ -31,13 +31,15 @@ saw, without the timing distortion of inline printing. `args_dump.json`; there is no separate args-only manifest. Their final `dtype` is registered at submit time in a dump-only per-task side table and resolved by AICPU when writing each scalar dump record. -- **Cross-architecture.** Same flags and on-disk layout family on - `a2a3` and `a5`. Both runtimes are wired through. +- **Cross-architecture metadata.** The flags, manifest schema, and scalar + metadata are wired through both runtime variants on `a2a3` and `a5`. + Real tensor-payload consumption is currently supported only on the + `a2a3` platform family; see the platform scope below. Enable in one line (`2` = full dump, every task): ```bash -python tests/st//test_.py -p a5sim --dump-args 2 +python tests/st//test_.py -p a2a3sim --dump-args 2 ``` ## 3. How to Use @@ -51,23 +53,52 @@ python tests/st//test_.py -p a5sim --dump-args 2 | `0` (or flag absent) | off — zero overhead | | `1` (bare `--dump-args`) | **partial** — only args marked with `L0TaskArgs::dump(...)` (see §3.2) | | `2` (`--dump-args 2`) | **full** — every task's tensor inputs/outputs and scalar args | -| `3` (`--dump-args 3`) | **full, JSON only** — every task's tensor/scalar metadata (shape, dtype, strides, scalar values) to `args_dump.json`; no payload captured, no `.bin` file | +| `3` (`--dump-args 3`) | **full metadata** — every task's tensor/scalar metadata (shape, dtype, strides, scalar values) to `args_dump.json`; tensor payload is omitted by default and can be enabled selectively | + +> **Tensor-payload platform scope:** levels 1/2 and filtered level 3 are +> currently supported for payload consumers only on `a2a3` / `a2a3sim`. +> On `a5` / `a5sim`, the manifest and `.bin` sizes can look valid while tensor +> payload bytes are zero because of +> [#1560](https://github.com/hw-native-sys/simpler/issues/1560). Do not use an +> a5 dump as a source of tensor truth until that issue is fixed. A selected +> level-3 payload still produces `args.bin` on that platform family, but it is +> not trustworthy for tensor restoration or an L0 swimlane that restores it. +> Plain level-3 metadata, including inline scalar values, does not consume +> tensor payload and remains available. Level 3 exists for consumers that need only the per-task argument metadata, -not the tensor element data — e.g. feeding the manifest into tooling. The AICPU -skips the arena payload copy entirely (treating every tensor like a scalar -for payload purposes), so it incurs none of full mode's device→host -bandwidth or arena pressure, and the manifest's `bin_file` is `null` with -every `bin_size` at `0`. +not every tensor's element data — e.g. feeding the manifest into tooling. With +no payload selector, the AICPU skips the arena payload copy entirely, the +manifest's `bin_file` is `null`, and every `bin_size` is `0`. + +When a tool needs the real bytes of a few tensors, add one task func-id set and +one or more task-global `args[]` slots: + +```bash +python tests/st//test_.py -p a2a3 --dump-args 3 \ + --dump-args-payload-func-id 11,12 \ + --dump-args-payload-slot 6 \ + --dump-args-payload-slot 7 +``` + +The task selector compares sets: an active mix array `[11, 12, 12]` matches +`11,12`. Only tensor records belonging to a task with exactly that func-id set +and one of the selected slots carry payload; all task and argument metadata +still appears in JSON. Scalar values already live inline in level-3 metadata +and never need payload selection. The two payload options must be used together, +are valid only with level 3, accept at most three unique non-negative func IDs, +and support slots `0..63`. Their tensor-payload contract currently applies only +to `a2a3` / `a2a3sim`. They remain accepted on `a5` / `a5sim`, but its selected +`args.bin` is not a supported tensor source until #1560 is fixed. ```bash # Standalone runner -python tests/st//test_.py -p a5sim --dump-args 2 # full +python tests/st//test_.py -p a2a3sim --dump-args 2 # full python tests/st//test_.py -p a2a3 -d 0 --dump-args # partial (level 1) # pytest -pytest tests/st/ --platform a5sim --dump-args 2 -pytest examples/a5/host_build_graph/vector_example --platform a5sim --dump-args 2 +pytest tests/st/ --platform a2a3sim --dump-args 2 +pytest examples/a2a3/host_build_graph/vector_example --platform a2a3sim --dump-args 2 ``` The level sets `CallConfig::enable_dump_args` (0/1/2/3). The host then @@ -81,7 +112,8 @@ than in the profiling-flag bitmask. The on-device AICPU reads the storage base via `set_platform_dump_base()`, the enable bit via `set_dump_args_enabled(SIMPLER_GET_DFX_FLAG(...))`, and latches the mode in `dump_args_init()` from the header level (`PARTIAL` → selective, -`FULL_JSON_ONLY` → metadata-only, payload copy suppressed). Because the +`FULL_JSON_ONLY` → all metadata, payload copy suppressed unless the selector +matches). Because the level is decided host-side **before any task is dispatched**, it is latched up front — there is no dependence on task submission order. AICore executors read the same `SIMPLER_DFX_FLAG_DUMP_ARGS` @@ -149,8 +181,13 @@ before any dispatch — not inferred from the markers, so it never depends on task submission order. At level 1, tasks without a marker are skipped and marked tasks dump only their selected arguments. At level 2, the markers are ignored and every task's tensors/scalars are dumped. At level -3, every task's tensors/scalars are dumped as metadata only (no payload, -no `.bin`). With no +3, every task's tensors/scalars are dumped as metadata; payload remains off +unless `--dump-args-payload-func-id` and one or more +`--dump-args-payload-slot` selectors are supplied. Selective level-3 payload +capture currently supports the **a2a3 platform family and level-2 SceneTest +cases only** (the `3` here is the dump level, not SceneTest L3); SceneTest L3 +rejects the selectors explicitly, and a5 tensor payload remains unsupported +under [#1560](https://github.com/hw-native-sys/simpler/issues/1560). With no `--dump-args` (level 0) dump is off entirely. If you run at level 1 but place no `dump(...)` markers anywhere, the @@ -168,19 +205,23 @@ The dump artifacts land under the per-task output prefix / └── args_dump/ ├── args_dump.json # unified argument manifest (`--dump-args`) - └── args.bin # raw tensor payload (`--dump-args`) + └── args.bin # raw tensor payload (levels 1/2, or filtered level 3) ``` Filenames are fixed (no per-file timestamp) — the directory is the -per-task uniqueness boundary. `--dump-args` emits both files in the -same `args_dump/` directory. +per-task uniqueness boundary. Plain level 3 emits only `args_dump.json`; +levels 1/2 and filtered level 3 also emit `args.bin`. +On a5, a present and correctly sized `args.bin` does not establish payload +truth while [#1560](https://github.com/hw-native-sys/simpler/issues/1560) is +open. #### `args_dump.json` — Unified manifest `args_dump.json` is the manifest; its `bin_file` field points at -the sibling binary payload. At level 3 (full, JSON only) no payload is -captured: `bin_file` is `null`, no `.bin` is written, and every entry's -`bin_size` is `0`. +the sibling binary payload. A plain level-3 capture has `bin_file: null`, no +`.bin`, and `bin_size: 0` for every entry. A filtered level-3 capture has +`bin_file: "args.bin"` plus a top-level `payload_filter`; only selected tensor +records have a non-zero `bin_size`. Example manifest (one input tensor captured before dispatch): @@ -191,6 +232,8 @@ Example manifest (one input tensor captured before dispatch): "type": "logical_contiguous", "byte_order": "little_endian" }, + "dump_args_level": 2, + "payload_filter": null, "total_args": 1, "before_dispatch": 1, "after_completion": 0, @@ -225,6 +268,9 @@ Example manifest (one input tensor captured before dispatch): Key fields: +- `dump_args_level` / `payload_filter` — the capture mode and optional + level-3 selector. A selector is encoded as + `{"func_id": [11, 12], "arg_indices": [6, 7]}`. - `task_id` — runtime task identity. Use to correlate with swimlane / PMU output. - `func_id` — **array** of the task's active-subtask kernel ids (its mix @@ -372,7 +418,8 @@ DumpDataHeader (host init, AICPU reads) ├── num_dump_threads ├── records_per_buffer ├── magic = 0x44554D50 ("DUMP") -└── dump_args_level (DumpArgsLevel: 0=off, 1=partial, 2=full, 3=full_json_only; AICPU latches in dump_args_init) +├── dump_args_level (DumpArgsLevel: 0=off, 1=partial, 2=full, 3=full_json_only; AICPU latches in dump_args_init) +└── payload_filter (up to 3 func IDs + a 64-bit args[] slot mask; level 3 only) DumpBufferState[num_dump_threads] (per-thread) ├── free_queue {buffer_ptrs[SLOT_COUNT], head, tail} @@ -575,27 +622,26 @@ framework reference. a5's `ArgsDumpCollector` derives from `ProfilerBase` and shares the -split mgmt + collector shard structure with a2a3. The single behavioral +split mgmt + collector shard structure with a2a3. The architectural deviation from §5.4 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 +host-shadow `malloc()` and the framework synchronizes selected fields 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 (`DumpDataHeader` + per-thread -`DumpBufferState`) device → host at the top of every tick, then -pushes back only the fields host modified (advanced +`free_` / `copy_to_device` / `copy_from_device`). The mgmt loop refreshes +ready-queue indices and entries individually, pulls each popped +`DumpMetaBuffer` on demand, and 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 -`DumpMetaBuffer` is still pulled on demand inside -`ProfilerAlgorithms::process_entry`. The per-thread arena lives -outside the shm region, so `on_buffer_collected` issues an -additional `copy_from_device` for the arena bytes referenced by -the buffer's records. +It does not bulk-refresh the entire shared-memory region each tick. + +The current args collector still reads `arena_write_offset` from the +zero-initialized host shadow without first refreshing that field. It therefore +skips the arena copy and can serialize correctly sized zero payloads on both +`a5sim` and a5 onboard. This is tracked by +[#1560](https://github.com/hw-native-sys/simpler/issues/1560); until it is +fixed, a5 tensor payload is outside the supported args-dump contract. ```text HOST DEVICE @@ -615,7 +661,7 @@ the buffer's records. │ collector shards start │ │ AFTER_COMPLETION │ │ │ │ dump_arg_record() │ │ mgmt every 10us tick: │ │ if buffer full: │ -│ copy_from_device(shm) │<──memcpy─────<│ push ready entry, │ +│ refresh queue fields │<──memcpy─────<│ push ready entry, │ │ for each ready entry: │ │ pop next from free_q │ │ copy buf from device │<──memcpy─────<│ │ │ resolve host ptr │ │ dump_args_flush(): │ @@ -668,8 +714,9 @@ on a5 inherits the same CRTP base as a2a3 and parameterizes [`BufferPoolManager`](../../src/common/platform/include/host/buffer_pool_manager.h) with `DumpModule`. The only a5-specific glue is the 5-callback -`MemoryOps`, the per-tick shm mirror, and the on-demand arena copy -inside `on_buffer_collected`. +`MemoryOps`, targeted shared-field refreshes, and the intended on-demand arena +copy inside `on_buffer_collected`. The current arena-offset refresh defect is +the unsupported-payload limitation described above. a5 normally relies on per-thread AICPU flush (`dump_args_flush`) to move the current buffer into the ready queue. If a hang/op-timeout reaps AICPU @@ -686,9 +733,10 @@ before that flush runs, `reconcile_counters` recovers a non-empty | Buffer model | rotating pool (free + ready queues per thread) | identical | | Host threads | split mgmt + collector shards, streams during execution | identical | | Host-class shape | `ProfilerBase` | identical | -| Host transport | `halHostRegister` shared memory | host-shadow `malloc` + per-tick `rtMemcpy`/`memcpy` | +| Host transport | `halHostRegister` shared memory | host-shadow `malloc` + targeted `rtMemcpy`/`memcpy` | | `MemoryOps` callbacks | 3 (`alloc`, `reg`, `free_`) | 5 (+ `copy_to_device`, `copy_from_device`) | -| Arena access | direct via SVM | `copy_from_device` inside `on_buffer_collected` | +| Arena access | direct via SVM | Intended `copy_from_device` inside `on_buffer_collected`; currently blocked by #1560 | +| Tensor-payload support | Supported | Unsupported until #1560 is fixed | | `reconcile_counters` | recover leftover current buffers + dropped accounting | identical | | Lifecycle | `initialize` → `start` → `stop` → `reconcile_counters` → `export_dump_files` → `finalize` | identical | @@ -723,6 +771,8 @@ device. Three failure modes exist when dump buffers run out of space. All three surface in the JSON manifest plus the `dump_args_flush` log line so users can detect and diagnose them. +These payload-space rules describe the supported a2a3 path and the intended +a5 behavior; the current a5 tensor-payload defect in #1560 takes precedence. ### 7.1 Truncation (`truncated = true`) @@ -887,6 +937,13 @@ most likely at the default partial level (`--dump-args` = level 1) with no `L0TaskArgs::dump(...)` markers in the orchestration, so every task is skipped. Add markers (§3.2), or pass `--dump-args 2` for a full dump. +**a5 `args.bin` is non-empty but tensor values are zero.** This is the current +host-shadow synchronization defect tracked by +[#1560](https://github.com/hw-native-sys/simpler/issues/1560), not evidence +that the recorded tensors were actually zero. Use `a2a3` / `a2a3sim` for any +payload consumer. Plain level-3 metadata remains usable on a5 because it does +not read tensor bytes. + **Manifest has tasks but `tensors[]` is empty.** AICPU received a task whose `TensorInfo` was missing or inconsistent. Look for a `LOG_WARN` from `try_log_dump_args_layout_mismatch` — it diff --git a/docs/dfx/l0-swimlane-profiling.md b/docs/dfx/l0-swimlane-profiling.md index 334f06ae5..0745d84ab 100644 --- a/docs/dfx/l0-swimlane-profiling.md +++ b/docs/dfx/l0-swimlane-profiling.md @@ -44,15 +44,18 @@ captured args — zero hand-written shapes or scalars. `msprof op simulator` op. The cube sub-core runs the AIC member, the vec sub-cores run the AIV member(s) → a combined AIC+AIV swimlane. - **Zero-guess args** — the task's real Tensor descriptors and scalars - come from a JSON-only `--dump-args 3` capture (metadata + scalar - values, no `.bin` payload — all reconstruction needs). The dump's - `func_id` array gives the task's mix membership directly. + come from a `--dump-args 3` metadata capture. When a structured + control tensor also needs its real contents, repeatable + `--restore-arg SLOT` adds a task+slot payload filter to that same + level-3 capture and restores only the selected slots into the replay on + `a2a3` / `a2a3sim`. The dump's `func_id` array gives the task's mix + membership directly. - **Loop-count control (`--set-arg SLOT=VALUE`)** — when a kernel's loop trip count comes from a scalar or a control tensor, override it to shrink a runaway loop (so the camodel doesn't hang) or to fix a "fake-fast" zero-filled control tensor — without distorting the - per-iteration pipeline structure. Repeatable; default uses the real - dump values. See + per-iteration pipeline structure. Repeatable; scalars keep their real + dump values, while unselected tensor payloads default to zero. See [§7.2](#72---set-arg-floor-for-a-loop-count-without-distortion). - **Source-line attribution (`--debug-line` / `-g`)** — compile the kernel with `-g` (skipping the link strip) so the trace carries @@ -65,6 +68,8 @@ captured args — zero hand-written shapes or scalars. replay is camodel either way — so `a2a3sim` is the default and needs no NPU and no arch-precheck. Use onboard only for a kernel whose sync idiom (e.g. a manual `prod.record()`) compiles only for the device. + Tensor restoration is currently an a2a3-only capability; a5 metadata-only + and zero-filled replays remain available. - **Two trace outputs** — a native Insight `trace.json` and an auto-generated Perfetto-friendly variant (sub-laned + atomic flags; see [§3.4](#34-viewing--insight-vs-perfetto)). @@ -125,7 +130,7 @@ onboard `--platform`; a sim `--platform` uses no NPU until step 5): | Step | Action | Uses NPU | | ---- | ------ | -------- | | 1 | Read the test's `CALLABLE`; build a `func_id → (source, core_type)` table | No | -| 2 | Run `--dump-args 3` (JSON-only) → `args_dump.json` (or reuse via `--dump-json`) | Onboard only | +| 2 | Run `--dump-args 3`; `--restore-arg` adds selective task+slot payload capture → `args_dump.json` and, when needed, a small `args.bin` (or reuse via `--dump-json`) | Onboard only | | 3 | Select the task whose member set == `--func-id`, reconstruct its full positional args, **print the arg-slot table** (slot / kind / shape / value) | No | | 4 | Emit the replay workspace and smoke-build it locally | No | | 5 | `msprof op simulator` collect + export → `trace.json`, then auto-converts a Perfetto variant | **Yes** | @@ -137,7 +142,7 @@ reading the kernel source — names are not in the dump (only kind / shape ```text [l0_swimlane] func_id=0 task=0x... mix=[0, 1, 2] mode=mix block_num=3 members=[MATMUL(aic,func 0), ADD(aiv,func 1), MUL(aiv,func 2)] -[l0_swimlane] arg slots (override with --set-arg SLOT=VALUE): +[l0_swimlane] arg slots (override with --set-arg SLOT=VALUE or --restore-arg SLOT): slot 0 tensor FLOAT32 [16384] ... ``` @@ -145,6 +150,8 @@ reading the kernel source — names are not in the dump (only kind / shape A scalar slot holds the value directly (`--set-arg 4=4`); a tensor slot holds a pointer, so `--set-arg` fills its buffer (`--set-arg 4=512`). See [§7.2](#72---set-arg-floor-for-a-loop-count-without-distortion). +Use `--restore-arg 6` instead when a tensor contains a non-uniform +structure or lookup table that a single fill value cannot represent. ### 3.3 Key flags @@ -153,15 +160,27 @@ holds a pointer, so `--set-arg` fills its buffer (`--set-arg 4=512`). See | `--test ` | SceneTest test file (required) | | `--func-id A[,B,C]` | The task's **member set** (comma-separated func_ids), required. `--func-id 0` traces the single-kernel task `{0}`; `--func-id 0,1,2` traces that 3-way mix. The set must exactly match a dispatched task's `func_id` array (you wrote the orchestration, so you know the members) | | `--task-id ` | Which task instance to replay (default: lowest). Instances of the same mix shape are structurally identical | -| `--platform

` | Dump platform → arch / compile / SoC params (default `a2a3sim`). Sim (`a2a3sim` / `a5sim`) dumps with no NPU; onboard (`a2a3` / `a5`) dumps on `$TASK_DEVICE` (wrap the tool in `task-submit`). The replay is camodel regardless; geometry is identical, so prefer sim | +| `--platform

` | Dump platform → arch / compile / SoC params (default `a2a3sim`). Sim (`a2a3sim` / `a5sim`) dumps with no NPU; onboard (`a2a3` / `a5`) dumps on `$TASK_DEVICE` (wrap the tool in `task-submit`). The replay is camodel regardless; geometry is identical, so prefer sim. Tensor restoration is supported only on `a2a3` / `a2a3sim` | | `--device ` | NPU device for an onboard dump + collect. **Auto-supplied** — `task-submit` appends `--device ` (also `$TASK_DEVICE`). Sim platforms ignore it | | `--case ` | Pin the dump to one `CASES[*].name`. Omitting it auto-pins the first case that lists `--platform`; pass it to target a smaller case when that first one overflows the camodel. Accepts `ClassName::Case` | -| `--dump-json ` | Reuse an existing `args_dump.json`, skipping the dump re-run | -| `--set-arg SLOT=VALUE` | Override an arg by `args[]` slot. Scalar slot → rewrite value; tensor slot → fill its buffer (integer dtypes). Shrinks a loop count without distortion. Repeatable. Default: real dump values | -| `--spmd-block-num N` | `block_num` written into the synthesized SPMD context (slot 48). Default: the **selected** case's `block_dim` | +| `--dump-json ` | Reuse an existing `args_dump.json`, skipping the dump re-run. `--restore-arg` requires its selected record payload and sibling `args.bin` | +| `--restore-arg SLOT` | Restore one tensor's captured `before_dispatch` payload on `a2a3` / `a2a3sim`. Repeatable. A fresh run uses `--dump-args 3 --dump-args-payload-func-id ` plus one internal `--dump-args-payload-slot` per restore; scalar/output-only/missing/truncated/overwritten slots are rejected. Cannot target the same slot as `--set-arg`. Do not use with a5 payloads while #1560 is open | +| `--set-arg SLOT=VALUE` | Override an arg by `args[]` slot. Scalar slot → rewrite value; tensor slot → fill its buffer (integer dtypes). Shrinks a loop count without distortion. Repeatable. By default scalars retain dump values and tensor payloads are zero-filled | +| `--spmd-block-num N` | `block_num` written into the synthesized SPMD context (slot 48). Default: 1 | | `--debug-line` / `-g` | Compile with `-g` (skip strip) so the trace carries `debug_line` → Insight maps instructions to source lines | -| `--no-collect` | Generate + smoke-build only; do not take an NPU | +| `--no-collect` | Stop after workspace generation and smoke build; an onboard dump still uses its locked NPU | | `--max-time ` | `task-submit` budget (default 1800) | +| `--msprof-timeout ` | `msprof op simulator --timeout` application-run limit in **minutes**. The L0 tool always passes it; default 120, valid range 1–2880. Use a shorter value deliberately for large, repetitive workloads that only need a partial pipeline | + +The fresh selective-payload capture used by `--restore-arg` currently targets +the a2a3 platform family and level-2 SceneTest cases. Remote/SceneTest L3 +capture is not part of the L0 workflow and rejects these selectors. a5/a5sim +tensor payload is not trustworthy because of +[#1560](https://github.com/hw-native-sys/simpler/issues/1560), so it is outside +the current restore contract. An a5/a5sim selective-payload request still +produces `args.bin`, but an L0 swimlane that restores its tensor payload is +untrustworthy; this does not prevent a5 metadata-only or zero-filled L0 +replays. Per-arch build parameters are fixed in the tool's `ARCH_CONFIG`: @@ -200,7 +219,7 @@ The raw export is under `/insight_export/OPPROF_*/simulator/`. [`.claude/skills/insight-trace/SKILL.md`](../../.claude/skills/insight-trace/SKILL.md); here it is built into the tool. -### 3.5 Selecting a task / mix, and what to `--set-arg` +### 3.5 Selecting a task / mix, and what to initialize `--func-id` **is** the task's member set — you name the exact func_ids the task is made of, and the tool picks the task whose `func_id` array matches. @@ -226,14 +245,91 @@ the tensor **shape** (`shapes[0]`), which the dump captures truthfully, so **no `--set-arg` is needed** — the real count (one 128×128 tile) is already small. +Use `--restore-arg` only when the kernel reads non-uniform tensor contents +that affect control flow or addressing — for example a runtime-built tiling +structure. A fresh restore run uses dump level 3 and captures payload only for +the exact `--func-id` task set and explicitly selected slots. This is currently +supported only with `--platform a2a3` or `a2a3sim`; do not consume a5 tensor +payload while #1560 is open. Keep weight, activation, and KV-pool tensors +unselected. +Slots restored from their `before_dispatch` snapshot cannot also use +`--set-arg`. + Omitting `--case` auto-pins the **first** `CASES[*]` that lists your `--platform`, so the dump always targets exactly one case (deterministic — no "run every case, reconstruct from the newest dump dir" ambiguity). Pass `--case` explicitly when that first case is not the smallest — a full-size -production case's shapes overflow the camodel replay (§3.4). The synthesized -slot-48 `block_num` comes from `--spmd-block-num` (default 1); an SPMD cohort sizes itself on device, so it is not readable from the test file. +production case's shapes can make the camodel impractical (§3.6). The +synthesized slot-48 `block_num` comes from `--spmd-block-num` (default 1); +the args dump does not carry that context, so consult the selected task's +orchestration for its real logical width. + +### 3.6 Oversized CAModel cases + +CAModel is a cycle-accurate, whole-chip simulator. The replay allocates every +tensor at the shape recorded in `args_dump.json`, including tensors whose +payload is left zero. Selective level-3 payload capture reduces `args.bin` +size and onboard dump pressure; it does **not** reduce replay allocation, +zero-initialization, or kernel loop work. A large KV pool or weight tensor can +therefore remain expensive even when only a small metadata slot is restored. + +Use this order: + +1. **Smoke-build before collecting.** Reuse the dump when available and pass + `--no-collect`. Step 3 prints every slot and shape, and the generated + workspace proves the sources and argument layout compile without spending + time in CAModel. +2. **Shrink shapes with `--case`.** Choose a bounded case that preserves the + target kernel's compile-time batch/head/tile geometry and mix membership, + while reducing shape-driving quantities such as layer count, page count, + cache rows, or sequence blocks. Changing only a runtime `seq_len` does not + help when allocation still uses a fixed `MAX_SEQ`, layer count, or cache + extent. +3. **Shrink only the loop when shapes are already acceptable.** Use + `--set-arg SLOT=VALUE` for scalar or uniform integer control inputs. It does + not resize tensor descriptors. Keep at least 3–4 iterations when the goal + is steady-state pipeline analysis. +4. **Restore only structured control inputs on a2a3/a2a3sim.** Use + `--restore-arg` for tiling structs or lookup tables; do not restore weights, + activations, or KV pools. Restoration preserves control truth but is not a + scaling mechanism. Do not consume A5 tensor payload while + [#1560](https://github.com/hw-native-sys/simpler/issues/1560) is open. +5. **Use a shorter timeout only when partial simulation is useful.** The + default is 120 minutes, so ordinary cases normally finish in full without + changing this option. For a large, repetitive case, + `--msprof-timeout 4` asks msProf to stop after four minutes and parse the + simulation data produced so far. This follows the + [Ascend CANN official QA/parameter guidance for msProf `--timeout`](https://www.hiascend.com/document/detail/zh/CANNCommunityEdition/82RC1alpha001/devaids/optool/atlasopdev_16_0082.html): + the option is intended for data-heavy, repeatedly computed operators whose + full simulation takes a long time, when a partial pipeline is enough to + obtain the necessary information. The official range is 1–2880 minutes. + +A timeout-generated trace is intentionally partial. It can answer questions +about an observed steady-state pipeline segment, but it cannot establish total +kernel duration or complete tail behavior, and the normal MMAD/FIX tail-count +self-check does not apply to a deliberately truncated run. If the timeout +fires before the target kernel produces any profiler data, msProf may have +nothing to parse and no `trace.json` will be available. + +Typical two-stage invocation: + +```bash +# Validate task selection, restored slots, shapes, and compilation. +python -m simpler_setup.tools.l0_swimlane ... \ + --case --dump-json --no-collect + +# Request a four-minute partial simulation for a large, repetitive case. +python -m simpler_setup.tools.l0_swimlane ... \ + --case --dump-json --msprof-timeout 4 +``` -### 3.6 Reusing a dump across kernels +If the workload has no bounded case, create a focused workload-local case that +submits the same target task with the same func-id set, argument order, tiling +path, and tile geometry, but smaller shape-driving extents. A one-off +diagnostic case should not be committed unless it provides durable regression +coverage. + +### 3.7 Reusing a dump across kernels The dump in step 2 is the slow part. When tracing several tasks from the same case, capture once and reuse: @@ -247,9 +343,13 @@ python -m simpler_setup.tools.l0_swimlane --test --func-id 3,4 --platform --dump-json outputs/__/args_dump/args_dump.json ``` -The manifest holds every task's args for the whole case. +The manifest holds every task's args for the whole case. The default level-3 +manifest is sufficient for descriptor/scalar reconstruction. Reusing it with +`--restore-arg` is rejected if it has no selected payload. Capture a fresh run +with `--restore-arg`, or reuse a filtered level-3 dump together with its +sibling `args.bin`. -### 3.7 Coverage across the #1181 test suite +### 3.8 Coverage across the #1181 test suite Commit `b1e4bd23` (#1181) touched ~70 test files. The `tensormap_and_ringbuffer` kernels among them fall into these l0 @@ -267,8 +367,8 @@ arch-precheck (the case must declare the `--platform` you pass — §3.1). | Single AIV | `vector_example` | `--func-id 0` | `kernel_add`, dispatched `rt_submit_aiv_task(0)` (vec only) | | Mix 2 AIV (per-lane) | `mixed_example` | `--func-id 3,4` | ADD_STD@AIV0 + MUL_STD@AIV1 (`get_subblockid` routing) | | Mix 3-way 1C2V | `mixed_example` | `--func-id 0,1,2` | MATMUL@AIC + ADD@AIV0 + MUL@AIV1 | -| SPMD single-source | `spmd_multiblock_aiv` | `--func-id 0` | single AIV reading `get_block_idx` (pass `--spmd-block-num`; replay traces block 0) | -| SPMD mix, 2 AIV share a source | `spmd_multiblock_mix` | `--func-id 0,1,2` | func 1 & 2 are distinct ids but **both `kernel_spmd_mix.cpp`** → the 2 AIV collapse to one (both lanes run it). Routes by `get_sub_block_id` (slot 49) → in replay both lanes read `sub_block_id=0`; AIV0/AIV1 differ only by write offset, so the pipeline stays representative. (The same-source collapse also covers the duplicate-func_id `[0,1,1]` shape an SPMD mix produces when `aiv0 = aiv1`.) | +| SPMD single-source | `spmd_multiblock_aiv` | `--func-id 0 --spmd-block-num 4` | single AIV reading `get_block_idx`; the default lowest task-id is orchestration task T0 with logical `block_num=4` | +| SPMD mix, 2 AIV share a source | `spmd_multiblock_mix` | `--func-id 0,1,2 --spmd-block-num 2` | the default lowest task-id is orchestration task T0 with logical `block_num=2`. Func 1 & 2 are distinct ids but **both `kernel_spmd_mix.cpp`** → the 2 AIV collapse to one (both lanes run it). Routes by `get_sub_block_id` (slot 49) → in replay both lanes read `sub_block_id=0`; AIV0/AIV1 differ only by write offset, so the pipeline stays representative. (The same-source collapse also covers the duplicate-func_id `[0,1,1]` shape an SPMD mix produces when `aiv0 = aiv1`.) | | Paged-attn, loop = scalar | `paged_attention_unroll` | `--func-id 0 --set-arg 4=4` | QK stage; `n_blocks` scalar (slot 4) → shrink to 4 ([§7.2](#72---set-arg-floor-for-a-loop-count-without-distortion)) | | Paged-attn, loop = control tensor | `batch_paged_attention` | `--func-id 1 --set-arg 1=512 --case CaseSmall1` | SF reads `context_lens` (**slot 1**) content (`aiv_softmax_prepare.cpp`); `--set-arg 1=512` fills it uniformly → shrinks the derived per-batch block count | @@ -276,7 +376,7 @@ Runnable commands (one per category): ```bash T=tests/st/a2a3/tensormap_and_ringbuffer # most representatives -E=examples/a2a3/tensormap_and_ringbuffer # vector_example / qwen3 +E=examples/a2a3/tensormap_and_ringbuffer # vector_example # --- a2a3sim cases (case declares a2a3sim; dump takes no NPU) --- L0="python -m simpler_setup.tools.l0_swimlane --platform a2a3sim -g" # -g: source-line attribution @@ -287,9 +387,9 @@ task-submit --device auto --max-time 1800 --run "$L0 --func-id 3,4 --test $T/m # Mix 3-way 1C2V — MATMUL + ADD + MUL task-submit --device auto --max-time 1800 --run "$L0 --func-id 0,1,2 --test $T/mixed_example/test_mixed_example.py" # SPMD single-source -task-submit --device auto --max-time 1800 --run "$L0 --func-id 0 --test $T/spmd_multiblock_aiv/test_spmd_multiblock_aiv.py" +task-submit --device auto --max-time 1800 --run "$L0 --func-id 0 --spmd-block-num 4 --test $T/spmd_multiblock_aiv/test_spmd_multiblock_aiv.py" # SPMD mix, 2 AIV share a source -task-submit --device auto --max-time 1800 --run "$L0 --func-id 0,1,2 --test $T/spmd_multiblock_mix/test_spmd_multiblock_mix.py" +task-submit --device auto --max-time 1800 --run "$L0 --func-id 0,1,2 --spmd-block-num 2 --test $T/spmd_multiblock_mix/test_spmd_multiblock_mix.py" # Paged-attn, loop = control tensor (context_lens = slot 1; fill it to shrink the per-batch block count) task-submit --device auto --max-time 1800 --run "$L0 --func-id 1 --set-arg 1=512 --case CaseSmall1 --test $T/batch_paged_attention/test_batch_paged_attention.py" @@ -306,24 +406,14 @@ task-submit --device auto --max-time 1800 --run "$L0a --func-id 0 --set-arg 4=4 `qwen3_14b_decode` used to serve as the "real SPMD workload" row here, driving its generated `fa_fused` mix with `--set-arg 0=96` on the `fa_total` work-item count. That kernel is gone: its attention is now a CANN -`FusedInferAttentionScore` extern, so the row was dropped. - -Being an extern is not what blocks it — the replay feeds `kernel_entry` and does -not care whether the source was generated or hand-written. The blocker is that -this one takes its work distribution from a `FAInferTilingData` **struct**, in a -`UINT8 metadata` buffer that `paged_attention_tiling_cce` fills at runtime. -`--set-arg` writes one integer into *every* element, so it cannot synthesize a -coherent struct (it is not even refused — `UINT8` passes the integer-dtype -check), and this tool dumps at level 3, which captures metadata but no payload -([args-dump](args-dump.md) §levels). With a zeroed `metadata`, -`fai_body.hpp` reads `tiling->needCoreNum == 0` and takes the degenerate branch, -so the trace would be unrepresentative rather than absent. - -That gap is in the tooling, not the kernel: `--dump-args 2` does capture the -buffer's real bytes. Restoring payloads for control tensors — rather than only -uniform-filling them — would make this and any other metadata-driven extern -replayable. Until then the "real SPMD workload" category has no representative -here. +`FusedInferAttentionScore` extern. Being an extern is not a blocker — the replay +still feeds `kernel_entry`. Its work distribution now comes from a +`FAInferTilingData` struct in the slot-6 `UINT8 metadata` buffer that +`paged_attention_tiling_cce` fills at runtime. `--restore-arg 6` can capture +that buffer through the selective level-3 payload filter and restore its real +bytes without trying to synthesize a struct through uniform `--set-arg` +filling. The committed production case is too large for a practical +cycle-accurate replay, so it is not listed as a runnable coverage case here. **Not l0 targets (excluded).** Runtime-mechanics tests (`orch_so_cache`, `prepared_callable`, `dynamic_register`, `l3_group`, `l3_dependency`, @@ -399,9 +489,11 @@ correctness-critical details: - **Buffer size uses the extent formula** `(start_offset + 1 + Σ(shape[i]-1)*stride[i]) * elem_size`, not `numel` — strided / offset views read past `numel`. -- Replay data is **memset to 0**; only the descriptor metadata is real. - Data-dependent branches / addresses can distort while pure pipeline - structure stays faithful (see [§8](#8-fidelity-rules)). +- Tensor payloads default to **memset 0**. `--restore-arg` reads the selected + logical-contiguous payload from `args.bin`, scatters it through the original + shape/strides/start-offset view, embeds the physical bytes in + `replay_host.cpp`, and initializes that device buffer with `aclrtMemcpy`. + `--set-arg` remains the uniform-fill alternative. ### 5.3 Build & collect @@ -462,12 +554,17 @@ orchestration, so `replay_host.cpp` **synthesizes** it: one pointed at slots 48/49. This is harmless for positional kernels (they ignore 48/49) and required for SPMD kernels that read `get_block_idx` / `get_block_num` (which would otherwise dereference null). `block_idx=0` -traces a representative block; `block_num` (from `--spmd-block-num`, default - -1) keeps steady-state branches (`block_idx+1 < block_num`) on their normal -path — see [§8](#8-fidelity-rules). An SPMD cohort sizes itself from -`rt_available_cluster_count()` on device, so the width is not readable from -the test file and must be passed. +traces a representative block. The default `block_num=1` models a single-block +replay and does **not** take multi-block branches such as +`block_idx+1 < block_num`. That default is sufficient only when the kernel +ignores `block_num`. The args dump does not record the synthesized slot-48 +context, so read the selected task's logical grid width from its orchestration +and supply it with `--spmd-block-num`. Some workloads derive that value from +`rt_available_cluster_count()`; others set an explicit task-specific value. +Pass the selected task's real value for faithful branch and grid-stride +behavior. A value of at least 2 can exercise the next-block branch as a bounded +approximation when the real value is unavailable, but it is not a substitute +for that value. Note the per-AIV-lane routing for a mix uses the hardware `get_subblockid()` (§5.4), not the synthesized slot-49 value. @@ -558,9 +655,9 @@ do not draw timing conclusions; retry with a different `n_blocks`. | Case selection (`--case`) | Pick a *scaled-down* case | Faithful if it keeps the tile geometry (just fewer blocks / shorter sequence); a case that changes tile M/K/N / head_dim traces only itself, not production | | Scalar values (`scale` / offsets …) | Use real dump values | Wrong value → wrong branch → distorted | | Loop count (`n_blocks`, via `--set-arg`) | Shrinkable to ≥ 3–4 | Faithful at ≥ 3–4; `= 1` distorts (§7.2) | -| Data filled to 0 | Default (memset 0) | Data-dependent branches / addresses distort; pure pipeline structure is fine | +| Tensor contents | Zero by default; on a2a3/a2a3sim, `--restore-arg` selected control tensors | Unrestored data-dependent branches/addresses can distort; supported restored slots use their real `before_dispatch` bytes. A5 restoration is excluded by #1560 | | SPMD `block_idx` (slot 48) | Fixed 0 | Traces a real block 0 — representative for uniform SPMD | -| SPMD `block_num` (slot 48) | Default `block_dim`; `--spmd-block-num` | Any value ≥ 2 keeps steady-state branches | +| SPMD `block_num` (slot 48) | Default 1; pass the selected task's real logical width with `--spmd-block-num` when the kernel reads it | `1` is a single-block path; ≥ 2 only approximates a multi-block branch, while the selected task's real value preserves branch and grid-stride behavior | | Per-AIV-lane routing (`get_subblockid`) | Automatic | Faithful — lanes run their real kernels (§6) | | Cross-core sync timing | Not modeled | **Optimistic** — sub-cores appear freely parallel (§9 tier C) | | Cross-platform (a2a3 vs a5) | Set by target | Instructions / timing genuinely differ (real silicon — §7.3) | @@ -585,8 +682,15 @@ do not draw timing conclusions; retry with a different `n_blocks`. reality. - **Simulation clock, not silicon.** Use it for *relative* per-pipe / per-arch structure, not absolute-latency claims. -- **Replay data is zero.** Only descriptor metadata is real; data-driven - control flow can diverge (§8). +- **Unselected tensor payloads are zero.** On a2a3/a2a3sim, restore structured + control inputs with `--restore-arg`; other data-driven control flow can + still diverge (§8). +- **A5 restored-payload L0 traces are untrustworthy.** While + [#1560](https://github.com/hw-native-sys/simpler/issues/1560) is open, do + not treat A5 args-dump payload as tensor truth or use it with + `--restore-arg`. A5 still writes a selected level-3 `args.bin`, but an L0 + swimlane restored from it is untrustworthy; metadata-only and zero-filled L0 + replays remain usable. - **Tail-truncation collection bug.** Validate `MMAD`/`FIX` counts every run (§7.4). - **1C2V only.** The mix path assumes 1 AIC + up to 2 AIV (the only @@ -608,6 +712,17 @@ disagrees with the dispatched payload, so the dump skipped it — see [§3.1](#31-prerequisites-one-time-per-test-case) and [args-dump.md](args-dump.md). +**`--restore-arg` says the manifest has no `bin_file`.** The reused dump +was captured without selective payload. Capture a fresh run with +`--restore-arg` (the tool adds the level-3 task+slot selector automatically), +or reuse a payload-carrying manifest together with its sibling `args.bin`. + +**A5 `--restore-arg` reads zero payload.** A5 tensor payload restoration is +outside the supported contract while +[#1560](https://github.com/hw-native-sys/simpler/issues/1560) is open. Use +`a2a3` / `a2a3sim` for tensor truth; A5 metadata-only or zero-filled replay +remains available. + **Smoke build fails on a missing symbol.** `replay_entry` / `launch_replay` must appear in `libreplay_kernel.so`. A wrong `--platform` picks the wrong `ARCH_CONFIG`. @@ -630,7 +745,7 @@ Trace each AIV kernel as its own single-kernel task instead (e.g. - [`.claude/skills/l0-swimlane/SKILL.md`](../../.claude/skills/l0-swimlane/SKILL.md) — the operating procedure for this tool (picking `--func-id` / - `--set-arg` / `--spmd-block-num`). + `--restore-arg` / `--set-arg` / `--spmd-block-num`). - [l2-swimlane-profiling.md](l2-swimlane-profiling.md) — the per-task / scheduler swimlane one level up. - [args-dump.md](args-dump.md) — the `func_id`-array-tagged per-task arg diff --git a/python/bindings/task_interface.cpp b/python/bindings/task_interface.cpp index 4d588551a..d5336adfe 100644 --- a/python/bindings/task_interface.cpp +++ b/python/bindings/task_interface.cpp @@ -31,6 +31,7 @@ #include #include +#include #include #include #include @@ -1245,6 +1246,65 @@ NB_MODULE(_task_interface, m) { return os.str(); }); + nb::class_(m, "DumpArgsPayloadFilter") + .def(nb::init<>()) + .def_prop_rw( + "func_ids", + [](const DumpArgsPayloadFilter &self) { + return std::vector(self.func_ids, self.func_ids + self.func_count); + }, + [](DumpArgsPayloadFilter &self, std::vector func_ids) { + std::sort(func_ids.begin(), func_ids.end()); + func_ids.erase(std::unique(func_ids.begin(), func_ids.end()), func_ids.end()); + if (func_ids.size() > DUMP_ARGS_PAYLOAD_MAX_FUNC_IDS) { + throw std::invalid_argument("DumpArgsPayloadFilter.func_ids accepts at most 3 unique ids"); + } + std::fill(std::begin(self.func_ids), std::end(self.func_ids), 0); + self.func_count = static_cast(func_ids.size()); + std::copy(func_ids.begin(), func_ids.end(), self.func_ids); + } + ) + .def_prop_rw( + "slots", + [](const DumpArgsPayloadFilter &self) { + std::vector slots; + for (uint32_t slot = 0; slot < DUMP_ARGS_PAYLOAD_ARG_MASK_BITS; ++slot) { + if ((self.arg_mask & (uint64_t{1} << slot)) != 0) { + slots.push_back(slot); + } + } + return slots; + }, + [](DumpArgsPayloadFilter &self, const std::vector &slots) { + self.arg_mask = 0; + for (uint32_t slot : slots) { + if (slot >= DUMP_ARGS_PAYLOAD_ARG_MASK_BITS) { + throw std::invalid_argument("DumpArgsPayloadFilter.slots entries must be in [0, 63]"); + } + self.arg_mask |= uint64_t{1} << slot; + } + } + ) + .def("validate", &DumpArgsPayloadFilter::validate) + .def("__repr__", [](const DumpArgsPayloadFilter &self) { + std::ostringstream os; + os << "DumpArgsPayloadFilter(func_ids=["; + for (uint32_t i = 0; i < self.func_count; ++i) { + if (i != 0) os << ", "; + os << self.func_ids[i]; + } + os << "], slots=["; + bool first = true; + for (uint32_t slot = 0; slot < DUMP_ARGS_PAYLOAD_ARG_MASK_BITS; ++slot) { + if ((self.arg_mask & (uint64_t{1} << slot)) == 0) continue; + if (!first) os << ", "; + os << slot; + first = false; + } + os << "])"; + return os.str(); + }); + // --- CallConfig --- nb::class_(m, "CallConfig") .def(nb::init<>()) @@ -1261,6 +1321,16 @@ NB_MODULE(_task_interface, m) { }, nb::rv_policy::reference_internal ) + .def_prop_rw( + "dump_args_payload_filter", + [](CallConfig &c) -> DumpArgsPayloadFilter & { + return c.dump_args_payload_filter; + }, + [](CallConfig &c, const DumpArgsPayloadFilter &filter) { + c.dump_args_payload_filter = filter; + }, + nb::rv_policy::reference_internal + ) .def_prop_rw( "enable_l2_swimlane", [](const CallConfig &c) { @@ -1343,6 +1413,15 @@ NB_MODULE(_task_interface, m) { append_ring_values(os, "runtime_env.ring_heap", true, self.runtime_env.ring_heap); append_ring_values(os, "runtime_env.ring_dep_pool", true, self.runtime_env.ring_dep_pool); } + if (self.dump_args_payload_filter.enabled()) { + os << ", dump_args_payload_filter.func_ids=["; + for (uint32_t i = 0; i < self.dump_args_payload_filter.func_count; ++i) { + if (i != 0) os << ", "; + os << self.dump_args_payload_filter.func_ids[i]; + } + os << "], dump_args_payload_filter.arg_mask=0x" << std::hex << self.dump_args_payload_filter.arg_mask + << std::dec; + } if (self.output_prefix_set()) { os << ", output_prefix='" << self.output_prefix << "'"; } diff --git a/python/simpler/worker.py b/python/simpler/worker.py index 5535007aa..5fb56452f 100644 --- a/python/simpler/worker.py +++ b/python/simpler/worker.py @@ -172,10 +172,12 @@ def my_l4_orch(orch, args, config): # 6 int32 (aicpu_thread_num, enable_l2_swimlane, enable_dump_args, # enable_pmu, enable_dep_gen, enable_scope_stats) + uint64 ring sizing # overrides (3 per-ring arrays of RUNTIME_ENV_RING_COUNT: ring_task_window, -# ring_heap, ring_dep_pool) + 1024-byte NUL-terminated output_prefix. Log config -# travels separately via ChipWorker.init(log_level) — not on per-task wire. +# ring_heap, ring_dep_pool) + DumpArgsPayloadFilter (three int32 func ids, +# uint32 count, uint64 mask) + 1024-byte NUL-terminated output_prefix. Log +# config travels separately via ChipWorker.init(log_level) — not on per-task +# wire. _RUNTIME_ENV_UINT64_FIELD_COUNT = 3 * RUNTIME_ENV_RING_COUNT -_CFG_FMT = struct.Struct("=iiiiii" + ("Q" * _RUNTIME_ENV_UINT64_FIELD_COUNT) + "1024s") +_CFG_FMT = struct.Struct("=iiiiii" + ("Q" * _RUNTIME_ENV_UINT64_FIELD_COUNT) + "iiiIQ1024s") # Args region starts after CONFIG, rounded up to 8 bytes so the first # Tensor.data (uint64_t at OFF_ARGS+8) is 8-byte aligned, avoiding # SIGBUS on strict-alignment platforms (aarch64 atomics, some ARM cores). @@ -1791,16 +1793,14 @@ def _chip_process_loop( # noqa: PLR0913 -- fork-child entry: all context (bins, def _read_config_from_mailbox(buf: memoryview) -> CallConfig: """Reconstruct a CallConfig from the unified mailbox layout.""" - ( - aicpu_tn, - swl, - dt, - pmu, - dep_gen, - scope_stats, - *ring_values, - prefix_bytes, - ) = _CFG_FMT.unpack_from(buf, _OFF_CONFIG) + fields = _CFG_FMT.unpack_from(buf, _OFF_CONFIG) + aicpu_tn, swl, dt, pmu, dep_gen, scope_stats = fields[:6] + ring_values = fields[6 : 6 + _RUNTIME_ENV_UINT64_FIELD_COUNT] + filter_offset = 6 + _RUNTIME_ENV_UINT64_FIELD_COUNT + payload_func_ids = list(fields[filter_offset : filter_offset + 3]) + payload_func_count = fields[filter_offset + 3] + payload_arg_mask = fields[filter_offset + 4] + prefix_bytes = fields[filter_offset + 5] ring_task_window = list(ring_values[:RUNTIME_ENV_RING_COUNT]) ring_heap = list(ring_values[RUNTIME_ENV_RING_COUNT : 2 * RUNTIME_ENV_RING_COUNT]) ring_dep_pool = list(ring_values[2 * RUNTIME_ENV_RING_COUNT : 3 * RUNTIME_ENV_RING_COUNT]) @@ -1814,6 +1814,8 @@ def _read_config_from_mailbox(buf: memoryview) -> CallConfig: cfg.runtime_env.ring_task_window = ring_task_window cfg.runtime_env.ring_heap = ring_heap cfg.runtime_env.ring_dep_pool = ring_dep_pool + cfg.dump_args_payload_filter.func_ids = payload_func_ids[:payload_func_count] + cfg.dump_args_payload_filter.slots = [slot for slot in range(64) if payload_arg_mask & (1 << slot)] # NUL-terminated C string in a 1024-byte field. cfg.output_prefix = prefix_bytes.split(b"\x00", 1)[0].decode("utf-8") return cfg diff --git a/simpler_setup/scene_test.py b/simpler_setup/scene_test.py index c87df2c0a..e57ba175d 100644 --- a/simpler_setup/scene_test.py +++ b/simpler_setup/scene_test.py @@ -627,6 +627,36 @@ def _parse_case_selector(value: str) -> tuple[str | None, str | None]: return (None, value) +def _resolve_dump_args_payload_filter(dump_level, func_ids_spec, slots): + """Validate the optional Level-3 tensor-payload selector.""" + if func_ids_spec is None and not slots: + return None + if func_ids_spec is None or not slots: + raise ValueError("--dump-args-payload-func-id and --dump-args-payload-slot must be provided together") + if int(dump_level) != 3: + raise ValueError("selective tensor payload capture requires --dump-args 3") + try: + func_ids = sorted({int(value) for value in func_ids_spec.split(",")}) + except ValueError as exc: + raise ValueError("--dump-args-payload-func-id must be a comma-separated list of integers") from exc + if not func_ids or len(func_ids) > 3 or any(func_id < 0 for func_id in func_ids): + raise ValueError("--dump-args-payload-func-id requires 1-3 non-negative unique ids") + unique_slots = sorted(set(slots)) + invalid_slots = [slot for slot in unique_slots if slot < 0 or slot >= 64] + if invalid_slots: + raise ValueError(f"--dump-args-payload-slot must be in [0, 63] (got {invalid_slots})") + return {"func_ids": func_ids, "slots": unique_slots} + + +def _validate_dump_args_payload_scene_level(scene_level, payload_filter): + """Selective payload capture currently feeds the L2-to-L0 replay path only.""" + if scene_level == 3 and payload_filter is not None: + raise NotImplementedError( + "selective dump-args payload capture is not supported for SceneTest L3; " + "the current workflow supports L2 dump -> L0 replay only" + ) + + def _match_selectors(cls_name: str, case_name: str, selectors: list[tuple]) -> bool: """True if ``(cls_name, case_name)`` matches any selector (empty list means no selector filter).""" if not selectors: @@ -879,6 +909,7 @@ def run_class_cases( # noqa: PLR0913 -- shared layer-5 entry; kwargs mirror CLI enable_pmu, enable_dep_gen, enable_scope_stats, + dump_args_payload_filter=None, enable_swimlane_overhead=False, ): """Execute a pre-filtered list of cases for one class (layers 5-6). @@ -911,6 +942,7 @@ def run_class_cases( # noqa: PLR0913 -- shared layer-5 entry; kwargs mirror CLI enable_pmu=enable_pmu, enable_dep_gen=enable_dep_gen, enable_scope_stats=enable_scope_stats, + dump_args_payload_filter=dump_args_payload_filter, output_prefix=str(prefix) if diagnostics_on else "", ) finally: @@ -1121,6 +1153,7 @@ def _build_config( enable_pmu=0, enable_dep_gen=False, enable_scope_stats=False, + dump_args_payload_filter=None, *, output_prefix="", ): @@ -1142,6 +1175,9 @@ def _build_config( config.enable_pmu = enable_pmu # 0=disabled, >0=enabled with event type config.enable_dep_gen = enable_dep_gen config.enable_scope_stats = enable_scope_stats + if dump_args_payload_filter is not None: + config.dump_args_payload_filter.func_ids = dump_args_payload_filter["func_ids"] + config.dump_args_payload_filter.slots = dump_args_payload_filter["slots"] # `output_prefix` is required by CallConfig::validate() whenever any # diagnostic flag is enabled. Caller threads it down from the per-case # directory built by _build_output_prefix(). @@ -1179,8 +1215,10 @@ def _run_and_validate( # noqa: PLR0913 -- threads CLI diagnostic flags + case c enable_pmu=0, enable_dep_gen=False, enable_scope_stats=False, + dump_args_payload_filter=None, output_prefix="", ): + _validate_dump_args_payload_scene_level(self._st_level, dump_args_payload_filter) if self._st_level == 2: self._run_and_validate_l2( worker, @@ -1193,6 +1231,7 @@ def _run_and_validate( # noqa: PLR0913 -- threads CLI diagnostic flags + case c enable_pmu=enable_pmu, enable_dep_gen=enable_dep_gen, enable_scope_stats=enable_scope_stats, + dump_args_payload_filter=dump_args_payload_filter, output_prefix=output_prefix, ) elif self._st_level == 3: @@ -1223,6 +1262,7 @@ def _run_and_validate_l2( # noqa: PLR0913 -- threads CLI diagnostic flags + cas enable_pmu=0, enable_dep_gen=False, enable_scope_stats=False, + dump_args_payload_filter=None, output_prefix="", ): params = case.get("params", {}) @@ -1276,6 +1316,7 @@ def _run_and_validate_l2( # noqa: PLR0913 -- threads CLI diagnostic flags + cas enable_pmu=enable_pmu, enable_dep_gen=enable_dep_gen, enable_scope_stats=enable_scope_stats, + dump_args_payload_filter=dump_args_payload_filter, output_prefix=output_prefix, ) @@ -1413,6 +1454,11 @@ def test_run(self, st_platform, st_worker, request): enable_dep_gen = self._effective_enable_dep_gen(request, warn=True) enable_scope_stats = request.config.getoption("--enable-scope-stats", default=False) enable_swimlane_overhead = request.config.getoption("--enable-swimlane-overhead", default=False) + dump_args_payload_filter = _resolve_dump_args_payload_filter( + enable_dump_args, + request.config.getoption("--dump-args-payload-func-id", default=None), + request.config.getoption("--dump-args-payload-slot", default=[]), + ) if rounds > 1: if enable_l2_swimlane: logger.warning("Profiling disabled: --rounds > 1") @@ -1420,6 +1466,7 @@ def test_run(self, st_platform, st_worker, request): if enable_dump_args: logger.warning("Dump args disabled: --rounds > 1") enable_dump_args = 0 + dump_args_payload_filter = None if enable_pmu: logger.warning("PMU disabled: --rounds > 1") enable_pmu = 0 @@ -1467,6 +1514,7 @@ def test_run(self, st_platform, st_worker, request): enable_pmu=enable_pmu, enable_dep_gen=enable_dep_gen, enable_scope_stats=enable_scope_stats, + dump_args_payload_filter=dump_args_payload_filter, enable_swimlane_overhead=enable_swimlane_overhead, ) @@ -1536,7 +1584,22 @@ def run_module(module_name): # noqa: PLR0912, PLR0915 -- CLI parsing + dispatch default=0, help="Dump per-task args at runtime. Level: 0=off, 1=partial (only " "tasks marked via Arg::dump(...), default when given without a value), " - "2=full (all tasks), 3=full_json_only (all tasks, JSON metadata only, no .bin payload).", + "2=full (all tasks), 3=full_json_only (all tasks' JSON metadata; no payload unless a selective " + "payload filter is given).", + ) + parser.add_argument( + "--dump-args-payload-func-id", + default=None, + metavar="ID[,ID...]", + help="With --dump-args 3, capture tensor payload only for tasks whose active func-id set matches.", + ) + parser.add_argument( + "--dump-args-payload-slot", + action="append", + type=int, + default=[], + metavar="SLOT", + help="With --dump-args 3, capture this tensor args[] slot for the selected task. Repeatable.", ) parser.add_argument( "--enable-dep-gen", @@ -1603,6 +1666,14 @@ def run_module(module_name): # noqa: PLR0912, PLR0915 -- CLI parsing + dispatch ) args = parser.parse_args() configure_logging(args.log_level) + try: + args.dump_args_payload_filter = _resolve_dump_args_payload_filter( + args.dump_args, + args.dump_args_payload_func_id, + args.dump_args_payload_slot, + ) + except ValueError as exc: + parser.error(str(exc)) # Match the per-test kernel/orchestration compile to the runtime's # sanitizer, and require the runtime preloaded — same as conftest, since @@ -1635,6 +1706,12 @@ def run_module(module_name): # noqa: PLR0912, PLR0915 -- CLI parsing + dispatch if args.rounds > 1 and args.enable_dep_gen: logger.warning("dep_gen disabled: --rounds > 1") args.enable_dep_gen = False + if args.rounds > 1 and args.dump_args: + logger.warning("Dump args disabled: --rounds > 1") + args.dump_args = 0 + args.dump_args_payload_filter = None + args.dump_args_payload_func_id = None + args.dump_args_payload_slot = [] if args.rounds > 1 and args.enable_scope_stats: logger.warning("scope_stats disabled: --rounds > 1") args.enable_scope_stats = False @@ -1772,6 +1849,7 @@ def run_module(module_name): # noqa: PLR0912, PLR0915 -- CLI parsing + dispatch enable_pmu=args.enable_pmu, enable_dep_gen=args.enable_dep_gen, enable_scope_stats=args.enable_scope_stats, + dump_args_payload_filter=args.dump_args_payload_filter, enable_swimlane_overhead=args.enable_swimlane_overhead, ) print(f" {label} PASSED") @@ -1801,6 +1879,16 @@ def _dispatch_test_phases_standalone(module_name, selected_by_cls, args): # noq # Path to the user's test script — sys.argv[0] is the script they invoked. script = os.path.abspath(getattr(module, "__file__", sys.argv[0])) + if args.dump_args_payload_filter is not None: + l3_classes = sorted(cls.__name__ for cls in selected_by_cls if cls._st_level == 3) + if l3_classes: + print( + "ERROR: selective dump-args payload capture is not supported for " + f"SceneTest L3 classes: {', '.join(l3_classes)}", + file=sys.stderr, + ) + return False + common = ["-p", args.platform, "--manual", args.manual, "--log-level", args.log_level] if args.sanitizer != "none": common += ["--sanitizer", args.sanitizer] @@ -1812,6 +1900,10 @@ def _dispatch_test_phases_standalone(module_name, selected_by_cls, args): # noq common += ["--enable-l2-swimlane", str(args.enable_l2_swimlane)] if args.dump_args: common += ["--dump-args", str(args.dump_args)] + if args.dump_args_payload_func_id: + common += ["--dump-args-payload-func-id", args.dump_args_payload_func_id] + for slot in args.dump_args_payload_slot: + common += ["--dump-args-payload-slot", str(slot)] if args.enable_dep_gen: common.append("--enable-dep-gen") if args.enable_scope_stats: diff --git a/simpler_setup/tools/dump_viewer.py b/simpler_setup/tools/dump_viewer.py index 7caf2da64..aafee4ef3 100644 --- a/simpler_setup/tools/dump_viewer.py +++ b/simpler_setup/tools/dump_viewer.py @@ -280,8 +280,9 @@ def main(): with open(manifest_path) as f: manifest = json.load(f) - # full_json_only dumps (level 3) carry no payload: bin_file is null and - # there is no .bin to export from — listing metadata still works. + # Plain full_json_only dumps (level 3) carry no payload. A level-3 dump + # with a selective payload filter can still name args.bin, so always follow + # the manifest rather than inferring payload availability from the level. bin_name = manifest.get("bin_file", "args.bin") bin_path = (dump_dir / bin_name) if bin_name else None args_data = manifest.get("args", manifest.get("tensors", [])) diff --git a/simpler_setup/tools/l0_swimlane.py b/simpler_setup/tools/l0_swimlane.py index 076fadca8..ba576a46c 100644 --- a/simpler_setup/tools/l0_swimlane.py +++ b/simpler_setup/tools/l0_swimlane.py @@ -11,8 +11,9 @@ Given a SceneTest test file + platform + a comma-list of func_ids (the mix member set), this tool: - 1. runs (or reuses) a JSON-only args dump (`--dump-args 3`) to capture the - task's real per-arg metadata, + 1. runs (or reuses) an args dump to capture the task's real per-arg + metadata with `--dump-args 3`; `--restore-arg` additionally requests + payload bytes only for the selected task func-id set and tensor slots, 2. picks the task whose active-subtask set == `--func-id` and reconstructs its FULL positional args[] (shapes / dtypes / strides / start_offset / scalar values) from the #1181 dump; the task's func_id ARRAY is its mix membership, @@ -152,10 +153,11 @@ # one source. Detected by load_kernel_meta; everything else # (incl. independent kernels packed into a mix dispatch, like # mixed_example) goes through the AIC/AIV-only path. -# * hw_block_dim / block_num — the SPMD grid width, from `--spmd-block-num` -# (defaults to 1). Cohorts size themselves from -# rt_available_cluster_count() at run time, so the width is -# not statically known from the test file. +# * block_num — the selected SPMD task's logical grid width, from +# `--spmd-block-num` (defaults to 1). The args dump does not +# carry slot-48 context; read the real value from the +# orchestration, where it may be task-specific or derived +# from rt_available_cluster_count(). # * aiv_lanes_per_block — the arch's hardware subblockdim (ARCH_CONFIG). # The mix path additionally needs ONE incore to declare the full tensor # `signature` (so the dump captures the shared args) — a standard CALLABLE @@ -189,6 +191,7 @@ def load_kernel_meta(test_path: Path, func_id: int, platform: str): spec.loader.exec_module(module) from simpler_setup import SceneTestCase # noqa: PLC0415 + from simpler_setup.scene_test import _resolve_incore_include_dirs # noqa: PLC0415 classes = [ v @@ -213,11 +216,15 @@ def load_kernel_meta(test_path: Path, func_id: int, platform: str): for cls in classes: for inc in cls.CALLABLE.get("incores", []): fid = inc["func_id"] + extra_include_dirs = inc.get("extra_include_dirs", []) by_func[fid] = { "func_id": fid, "source": (test_path.parent / inc["source"]).resolve(), "core_type": inc["core_type"], "name": inc.get("name") or Path(inc["source"]).stem, + "extra_include_dirs": ( + _resolve_incore_include_dirs(extra_include_dirs, inc) if extra_include_dirs else [] + ), } owner_cls[fid] = cls if func_id not in by_func: @@ -247,7 +254,16 @@ def _case_from_manifest(manifest: Path, class_name: str) -> str: # --------------------------------------------------------------------------- # Step 2: obtain an args_dump.json (run the test in sim, or reuse one) # --------------------------------------------------------------------------- -def get_or_run_dump(test_path: Path, platform: str, variant: str, dump_json, case=None, device=None): +def get_or_run_dump( + test_path: Path, + platform: str, + variant: str, + dump_json, + case=None, + device=None, + payload_func_ids=None, + payload_slots=None, +): if dump_json: p = Path(dump_json) if not p.is_file(): @@ -256,12 +272,16 @@ def get_or_run_dump(test_path: Path, platform: str, variant: str, dump_json, cas outputs = PROJECT_ROOT / "outputs" before = set(outputs.glob("*/args_dump")) if outputs.is_dir() else set() - # Level 3 (full, JSON-only): every task's tensor *metadata* (shape/dtype/ - # strides) + scalar values, no .bin payload copy. That is exactly what arg - # reconstruction consumes (it never reads the payload), and it skips the - # device->host arena copy entirely — cheaper, and avoids the large-shape - # copy failing onboard. + # Level 3 always captures complete task/argument metadata. A restore adds a + # narrow tensor-payload filter to that same capture, avoiding the all-task + # payload volume and runtime pressure of level 2. cmd = [sys.executable, str(test_path), "-p", platform, "--dump-args", "3"] + if payload_slots: + if not payload_func_ids: + raise ValueError("payload_func_ids is required when payload_slots is set") + cmd += ["--dump-args-payload-func-id", ",".join(str(value) for value in payload_func_ids)] + for slot in payload_slots: + cmd += ["--dump-args-payload-slot", str(slot)] # Pin the dump to exactly one case, allowing it to be `manual` (l0_swimlane # tracing targets are often manual to stay out of CI). `case` is main's # resolved case: the explicit --case, else the auto-pinned first-platform @@ -391,6 +411,12 @@ def fids(t): "shape": shape, "strides": strides, "start_offset": int(t.get("start_offset", 0)), + "role": t.get("role"), + "stage": t.get("stage"), + "bin_offset": int(t.get("bin_offset", 0)), + "bin_size": int(t.get("bin_size", 0)), + "truncated": bool(t.get("truncated", False)), + "overwritten": bool(t.get("overwritten", False)), } ) for s in scalars: @@ -424,6 +450,118 @@ def _is_contiguous(shape, strides, start_offset): return start_offset == 0 +def _logical_numel(shape): + numel = 1 + for dim in shape: + if dim < 0: + raise ValueError(f"negative tensor dimension {dim}") + numel *= dim + return numel + + +def _materialize_physical_payload(logical_data, shape, strides, start_offset, elem_size): + """Scatter row-major logical dump bytes into the tensor's physical view.""" + if len(shape) != len(strides): + raise ValueError(f"shape/strides rank mismatch: {shape} vs {strides}") + if start_offset < 0 or any(stride < 0 for stride in strides): + raise ValueError(f"negative start_offset/stride is unsupported: start={start_offset}, strides={strides}") + + numel = _logical_numel(shape) + expected = numel * elem_size + if len(logical_data) != expected: + raise ValueError(f"logical payload size {len(logical_data)} does not match expected {expected}") + if expected == 0: + raise ValueError("zero-sized tensor payload cannot be restored") + + physical = bytearray((start_offset + _extent_elem(shape, strides)) * elem_size) + for linear_idx in range(numel): + rem = linear_idx + physical_elem = start_offset + for dim, stride in zip(reversed(shape), reversed(strides)): + coord = rem % dim + rem //= dim + physical_elem += coord * stride + src = linear_idx * elem_size + dst = physical_elem * elem_size + physical[dst : dst + elem_size] = logical_data[src : src + elem_size] + return bytes(physical) + + +def restore_arg_payloads(manifest: Path, kargs: list[dict], restore_slots): + """Load and materialize payloads for explicitly selected tensor slots.""" + if not restore_slots: + return + + data = json.loads(manifest.read_text()) + bin_format = data.get("bin_format") or {} + if bin_format.get("type") != "logical_contiguous" or bin_format.get("byte_order") != "little_endian": + raise ValueError("--restore-arg requires bin_format type=logical_contiguous and byte_order=little_endian") + + bin_name = data.get("bin_file") + if not bin_name: + raise ValueError( + "--restore-arg requires a payload-carrying dump; this manifest has no bin_file " + "(it was likely captured with --dump-args 3)" + ) + bin_path = manifest.parent / bin_name + if not bin_path.is_file(): + raise ValueError(f"--restore-arg payload file not found: {bin_path}") + + by_slot = {a["slot"]: a for a in kargs} + file_size = bin_path.stat().st_size + with open(bin_path, "rb") as payload_file: + for slot in restore_slots: + arg = by_slot.get(slot) + if arg is None: + raise ValueError(f"--restore-arg slot {slot} is not an arg of this task") + if arg["kind"] != "tensor": + raise ValueError(f"--restore-arg slot {slot} is a scalar; only tensor payloads can be restored") + if arg.get("stage") != "before_dispatch": + raise ValueError( + f"--restore-arg slot {slot} has no before_dispatch payload; output-only tensors " + "cannot initialize a replay" + ) + if arg.get("truncated"): + raise ValueError(f"--restore-arg slot {slot} payload is truncated") + if arg.get("overwritten"): + raise ValueError(f"--restore-arg slot {slot} payload was overwritten before export") + + dtype = arg["dtype"] + if dtype not in DTYPE_SIZE: + raise ValueError(f"--restore-arg slot {slot} has unsupported dtype {dtype}") + expected_size = _logical_numel(arg["shape"]) * DTYPE_SIZE[dtype] + bin_offset = arg["bin_offset"] + bin_size = arg["bin_size"] + if bin_size != expected_size: + raise ValueError( + f"--restore-arg slot {slot} bin_size {bin_size} does not match logical tensor size {expected_size}" + ) + if bin_offset < 0 or bin_offset + bin_size > file_size: + raise ValueError( + f"--restore-arg slot {slot} payload range [{bin_offset}, {bin_offset + bin_size}) " + f"exceeds {bin_path} size {file_size}" + ) + + payload_file.seek(bin_offset) + logical_data = payload_file.read(bin_size) + if len(logical_data) != bin_size: + raise ValueError( + f"--restore-arg slot {slot} short read: expected {bin_size} bytes, got {len(logical_data)}" + ) + arg["restore_data"] = _materialize_physical_payload( + logical_data, + arg["shape"], + arg["strides"], + arg["start_offset"], + DTYPE_SIZE[dtype], + ) + print( + f"[l0_swimlane] restoring tensor slot {slot} " + f"({dtype} {arg['shape']}): {bin_size} logical bytes -> " + f"{len(arg['restore_data'])} physical bytes" + ) + + # --------------------------------------------------------------------------- # Step 4: code-generation for the 5 workspace files # --------------------------------------------------------------------------- @@ -639,14 +777,27 @@ def _emit_tensor_alloc_descs(args): ndims = len(shape) shp = ", ".join(str(x) for x in shape) strd = ", ".join(str(x) for x in strides) - # Default: data memset to 0 (only descriptor metadata is real). When - # --set-arg fills this tensor, write VALUE into every element instead — - # for control tensors whose CONTENT drives the kernel (e.g. paged - # attention reads n_blocks from the context_lens tensor). The low `esz` - # bytes of the int64 VALUE are copied per element (correct for any - # integer width, little-endian). + # Tensor initialization has three exclusive modes: restored dump bytes, + # a --set-arg uniform integer fill, or zero. fill = a.get("fill") - if fill is None: + restore_data = a.get("restore_data") + if fill is not None and restore_data is not None: + raise ValueError(f"tensor slot {slot} cannot be both restored and uniformly filled") + if restore_data is not None: + byte_rows = [] + for off in range(0, len(restore_data), 16): + byte_rows.append(" " + ", ".join(f"0x{b:02x}" for b in restore_data[off : off + 16])) + byte_literal = ",\n".join(byte_rows) + init = ( + f" {{\n" + f" static const unsigned char hbuf{ti}[] = {{\n" + f"{byte_literal}\n" + f" }};\n" + f" ACL_CHECK(aclrtMemcpy(d_t{ti}, t{ti}Bytes, hbuf{ti}, sizeof(hbuf{ti}),\n" + f" ACL_MEMCPY_HOST_TO_DEVICE));\n" + f" }}" + ) + elif fill is None: init = f" ACL_CHECK(aclrtMemset(d_t{ti}, t{ti}Bytes, 0, t{ti}Bytes));" else: init = ( @@ -758,8 +909,9 @@ def emit_replay_host(tensor_count: int, args, block_num: int = 1) -> str: // positional kernels (they ignore 48/49); required for SPMD kernels that // read get_block_idx / get_block_num / get_sub_block_id, which would // otherwise dereference a null context. block_idx=0 traces a representative - // block; block_num={block_num} (from --spmd-block-num) keeps steady-state - // branches (e.g. `block_idx+1 < block_num`) on their normal path. + // block. block_num={block_num} comes from --spmd-block-num; 1 models a + // single-block path, while SPMD branch/grid-stride fidelity requires the + // real grid width. uint8_t h_local[64] = {{0}}; // LocalContext: block_idx@0, block_num@4 *reinterpret_cast(h_local + 0) = 0; *reinterpret_cast(h_local + 4) = {block_num}; @@ -794,11 +946,12 @@ def emit_replay_host(tensor_count: int, args, block_num: int = 1) -> str: """ -def emit_cmakelists(arch: str, name: str, cfg, debug: bool = False) -> str: +def emit_cmakelists(arch: str, name: str, cfg, debug: bool = False, extra_include_dirs=None) -> str: # With -g, also drop the linker `-s` (strip) so the device kernel's # debug_line survives -> Insight can map instructions to source lines. link_opts = "-Wl,-z,relro -Wl,-z,now" if debug else "-s -Wl,-z,relro -Wl,-z,now" dbg_flag = "\n -g" if debug else "" + extra_includes = "\n".join(f' "{include_dir}"' for include_dir in (extra_include_dirs or [])) return f"""\ cmake_minimum_required(VERSION 3.16) @@ -850,6 +1003,7 @@ def emit_cmakelists(arch: str, name: str, cfg, debug: bool = False) -> str: ${{PTO_ISA_ROOT}}/include/pto ${{REPO_ROOT}}/src/{arch}/runtime/tensormap_and_ringbuffer/runtime ${{REPO_ROOT}}/src/{arch}/runtime/tensormap_and_ringbuffer/common + ${{REPO_ROOT}}/src/common/platform/include ${{REPO_ROOT}}/src/common/task_interface ${{REPO_ROOT}}/src/{arch}/platform/include ${{REPO_ROOT}}/simpler_setup/incore @@ -857,6 +1011,7 @@ def emit_cmakelists(arch: str, name: str, cfg, debug: bool = False) -> str: ${{ASCEND_HOME_PATH}}/pkg_inc/profiling ${{ASCEND_HOME_PATH}}/pkg_inc/runtime/runtime ${{ASCEND_HOME_PATH}}/include +{extra_includes} ) add_library(replay_kernel SHARED replay_kernel.cpp replay_launch.cpp) @@ -882,10 +1037,14 @@ def emit_cmakelists(arch: str, name: str, cfg, debug: bool = False) -> str: """ -def emit_run_collect(cfg, pto_isa_root: str) -> str: +def emit_run_collect(cfg, pto_isa_root: str, msprof_timeout: int = 120) -> str: # Plain string (bash uses ${} braces) — bake SoC default and the # pin-resolved pto-isa path via tokens (no ambient PTO_ISA_ROOT env #1403). - return _RUN_COLLECT_TEMPLATE.replace("__SOC_DEFAULT__", cfg["soc"]).replace("__PTO_ISA_ROOT__", pto_isa_root) + return ( + _RUN_COLLECT_TEMPLATE.replace("__SOC_DEFAULT__", cfg["soc"]) + .replace("__PTO_ISA_ROOT__", pto_isa_root) + .replace("__MSPROF_TIMEOUT__", str(msprof_timeout)) + ) _RUN_COLLECT_TEMPLATE = """\ @@ -917,7 +1076,7 @@ def emit_run_collect(cfg, pto_isa_root: str) -> str: msprof op simulator \\ --application="$BUILD_DIR/replay_host" --kernel-name="replay_entry" \\ - --launch-count=1 --soc-version="$SOC_VERSION" --timeout=120 \\ + --launch-count=1 --soc-version="$SOC_VERSION" --timeout=__MSPROF_TIMEOUT__ \\ --output="$COLLECT_DIR/out" 2>&1 | tee "$COLLECT_DIR/msprof_collect.log" OPPROF_DIR="$(find "$COLLECT_DIR/out" -maxdepth 1 -mindepth 1 -type d -name 'OPPROF_*' | sort | tail -n 1)" @@ -944,6 +1103,7 @@ def generate_workspace( # noqa: PLR0913 pto_isa_root: str, debug: bool = False, block_num: int = 1, + msprof_timeout: int = 120, ): ws.mkdir(parents=True, exist_ok=True) # One combined replay_entry for the whole mix task (cube=AIC member, vec=AIV @@ -953,9 +1113,12 @@ def generate_workspace( # noqa: PLR0913 (ws / "replay_kernel.cpp").write_text(emit_replay_kernel_combined(members, cfg)) (ws / "replay_launch.cpp").write_text(emit_replay_launch()) (ws / "replay_host.cpp").write_text(emit_replay_host(tensor_count, args, block_num)) - (ws / "CMakeLists.txt").write_text(emit_cmakelists(arch, name, cfg, debug)) + extra_include_dirs = list( + dict.fromkeys(include_dir for member in members for include_dir in member.get("extra_include_dirs", [])) + ) + (ws / "CMakeLists.txt").write_text(emit_cmakelists(arch, name, cfg, debug, extra_include_dirs=extra_include_dirs)) rc = ws / "run_collect.sh" - rc.write_text(emit_run_collect(cfg, pto_isa_root)) + rc.write_text(emit_run_collect(cfg, pto_isa_root, msprof_timeout)) rc.chmod(0o755) @@ -1197,6 +1360,20 @@ def collect(ws: Path, env, max_time: int, device=None, dest_name: str = "trace.j # --------------------------------------------------------------------------- +def parse_arg_overrides(set_arg, ap: argparse.ArgumentParser): + parsed = [] + for spec in set_arg: + if "=" not in spec: + ap.error(f"--set-arg must be SLOT=VALUE (got {spec!r})") + slot_s, val_s = spec.split("=", 1) + try: + slot, value = int(slot_s), int(val_s) + except ValueError: + ap.error(f"--set-arg SLOT and VALUE must be integers (got {spec!r})") + parsed.append((slot, value)) + return parsed + + def apply_arg_overrides(kargs: list[dict], set_arg, ap: argparse.ArgumentParser): """Apply --set-arg SLOT=VALUE overrides to reconstructed args. @@ -1210,15 +1387,10 @@ def apply_arg_overrides(kargs: list[dict], set_arg, ap: argparse.ArgumentParser) """ if not set_arg: return + if isinstance(set_arg[0], str): + set_arg = parse_arg_overrides(set_arg, ap) by_slot = {a["slot"]: a for a in kargs} - for spec in set_arg: - if "=" not in spec: - ap.error(f"--set-arg must be SLOT=VALUE (got {spec!r})") - slot_s, val_s = spec.split("=", 1) - try: - slot, value = int(slot_s), int(val_s) - except ValueError: - ap.error(f"--set-arg SLOT and VALUE must be integers (got {spec!r})") + for slot, value in set_arg: a = by_slot.get(slot) if a is None: ap.error(f"--set-arg slot {slot} is not an arg of this task") @@ -1288,8 +1460,24 @@ def main(): "the camodel replay — name the small one instead. Accepts " "ClassName::Case too.", ) - ap.add_argument("--dump-json", default=None, help="reuse an existing args_dump.json") + ap.add_argument( + "--dump-json", + default=None, + help="reuse an existing args_dump.json. --restore-arg additionally " + "requires the selected payload in the manifest's args.bin.", + ) # ----- replay tuning ----- + ap.add_argument( + "--restore-arg", + action="append", + default=[], + type=int, + metavar="SLOT", + help="initialize a tensor slot from its captured before_dispatch payload. " + "Repeatable. A fresh run uses --dump-args 3 plus a selective payload " + "filter; a reused --dump-json must reference args.bin. Intended for structured control tensors " + "that --set-arg cannot represent.", + ) ap.add_argument( "--set-arg", action="append", @@ -1303,7 +1491,8 @@ def main(): "(--set-arg 4=4); mix paged-attention derives n_blocks " "from the context_lens tensor (--set-arg 4=512 -> " "n_blocks=ceil(512/block_size)). Only real arg slots are " - "settable. Repeatable. Default: real dump values.", + "settable. Repeatable. By default scalars retain dump values and " + "tensor payloads are zero-filled.", ) ap.add_argument( "--spmd-block-num", @@ -1311,10 +1500,9 @@ def main(): default=None, metavar="N", help="block_num written into the synthesized SPMD LocalContext " - "(slot 48). Default: 1. Required to replay an SPMD cohort, whose " - "width is resolved on device. Only matters for " - "kernels that branch/stride on block_num; set the real grid " - "width for those.", + "(slot 48). Default: 1. The args dump does not carry this context. " + "For kernels that branch/stride on block_num, pass the selected " + "task's real logical grid width from its orchestration.", ) ap.add_argument( "--debug-line", @@ -1327,8 +1515,26 @@ def main(): # ----- run control ----- ap.add_argument("--no-collect", action="store_true", help="smoke build only") ap.add_argument("--max-time", type=int, default=1800, help="task-submit budget (sec)") + ap.add_argument( + "--msprof-timeout", + type=int, + default=120, + metavar="MINUTES", + help="msprof op simulator application timeout in minutes (default: 120; valid range: 1-2880)", + ) args = ap.parse_args() + if not 1 <= args.msprof_timeout <= 2880: + ap.error("--msprof-timeout MINUTES must be in [1, 2880]") + set_arg = parse_arg_overrides(args.set_arg, ap) + restore_slots = list(dict.fromkeys(args.restore_arg)) + invalid_restore_slots = [slot for slot in restore_slots if slot < 0 or slot >= KARGS_SLOTS] + if invalid_restore_slots: + ap.error(f"--restore-arg SLOT must be in [0, {KARGS_SLOTS - 1}] (got {invalid_restore_slots})") + conflicts = sorted(set(restore_slots) & {slot for slot, _ in set_arg}) + if conflicts: + ap.error(f"arg slot(s) {conflicts} cannot use both --restore-arg and --set-arg") + test_path = Path(args.test).resolve() arch, variant = parse_platform(args.platform) cfg = ARCH_CONFIG.get(arch) @@ -1348,23 +1554,35 @@ def main(): selected_case = args.case or meta["auto_case"] if not args.case and meta["auto_case"]: print(f"[l0_swimlane] no --case; auto-pinned first {args.platform} case: {meta['auto_case']}") - # block_num for the synthesized slot-48 LocalContext. An SPMD cohort sizes - # itself from rt_available_cluster_count() on device, so its width is not - # readable from the test file; pass --spmd-block-num to replay one. Default - # 1 is a non-SPMD single block. + # block_num for the synthesized slot-48 LocalContext is not present in the + # args dump. Pass the selected SPMD task's real logical width from its + # orchestration; default 1 is a non-SPMD single block. block_num = args.spmd_block_num if args.spmd_block_num is not None else 1 # task-submit hands the locked device by appending --device to argv # (and may also set $TASK_DEVICE). One resolved value threads through both # the dump and the collect so they share the single outer lock. device = args.device or os.environ.get("TASK_DEVICE") - manifest = get_or_run_dump(test_path, args.platform, variant, args.dump_json, selected_case, device) + manifest = get_or_run_dump( + test_path, + args.platform, + variant, + args.dump_json, + selected_case, + device, + payload_func_ids=func_id_list if restore_slots else None, + payload_slots=restore_slots, + ) print(f"[l0_swimlane] manifest: {manifest}") # Select the task whose member set == --func-id; reconstruct its full # positional payload. mix_func_ids is the dump's array (slot order # AIC,AIV0,AIV1), NOT the typed order, so lane assignment stays correct. chosen, tensor_count, kargs, mix_func_ids = reconstruct_task_args(manifest, func_id_list, args.task_id) + try: + restore_arg_payloads(manifest, kargs, restore_slots) + except ValueError as exc: + ap.error(str(exc)) # Resolve the mix members (slot order) to their sources/core_types. missing = [f for f in mix_func_ids if f not in by_func] @@ -1386,13 +1604,14 @@ def main(): # Full arg-slot map so the caller can pick a slot for --set-arg without # cross-referencing the kernel source. Names are not in the dump (only # kind/shape/value) — read the kernel's `args:` header for those. - print("[l0_swimlane] arg slots (override with --set-arg SLOT=VALUE):") + print("[l0_swimlane] arg slots (override with --set-arg SLOT=VALUE or --restore-arg SLOT):") for a in sorted(kargs, key=lambda x: x["slot"]): if a["kind"] == "tensor": - print(f" slot {a['slot']:<2} tensor {a['dtype']:<8} {a['shape']}") + restored = " [restored payload]" if "restore_data" in a else "" + print(f" slot {a['slot']:<2} tensor {a['dtype']:<8} {a['shape']}{restored}") else: print(f" slot {a['slot']:<2} scalar = {a['value']}") - apply_arg_overrides(kargs, args.set_arg, ap) + apply_arg_overrides(kargs, set_arg, ap) # Self-describing label: ____, so the # workspace dir and the trace.json filename say which case/kernel/mix they are. @@ -1416,6 +1635,7 @@ def main(): pto_isa_root, debug=args.debug_line, block_num=block_num, + msprof_timeout=args.msprof_timeout, ) print(f"[l0_swimlane] workspace: {ws}") diff --git a/src/a2a3/platform/onboard/host/device_runner.cpp b/src/a2a3/platform/onboard/host/device_runner.cpp index 981a21358..017a6da32 100644 --- a/src/a2a3/platform/onboard/host/device_runner.cpp +++ b/src/a2a3/platform/onboard/host/device_runner.cpp @@ -1072,7 +1072,8 @@ int DeviceRunner::init_args_dump(Runtime &runtime, int device_id) { }; int rc = dump_collector_.initialize( - num_dump_threads, device_id, alloc_cb, register_cb, free_cb, output_prefix_, dump_args_level_ + num_dump_threads, device_id, alloc_cb, register_cb, free_cb, output_prefix_, dump_args_level_, + dump_args_payload_filter_ ); if (rc != 0) { return rc; diff --git a/src/a2a3/platform/sim/host/device_runner.cpp b/src/a2a3/platform/sim/host/device_runner.cpp index 912f79324..c7c756e8c 100644 --- a/src/a2a3/platform/sim/host/device_runner.cpp +++ b/src/a2a3/platform/sim/host/device_runner.cpp @@ -772,7 +772,8 @@ int DeviceRunner::init_args_dump(Runtime &runtime, int device_id) { }; int rc = dump_collector_.initialize( - num_dump_threads, device_id, alloc_cb, nullptr, free_cb, output_prefix_, dump_args_level_ + num_dump_threads, device_id, alloc_cb, nullptr, free_cb, output_prefix_, dump_args_level_, + dump_args_payload_filter_ ); if (rc != 0) { return rc; diff --git a/src/a5/platform/onboard/host/device_runner.cpp b/src/a5/platform/onboard/host/device_runner.cpp index aceaed3c8..71b4a69b9 100644 --- a/src/a5/platform/onboard/host/device_runner.cpp +++ b/src/a5/platform/onboard/host/device_runner.cpp @@ -795,7 +795,7 @@ int DeviceRunner::init_args_dump(Runtime &runtime, int device_id) { int rc = dump_collector_.initialize( num_dump_threads, device_id, prof_alloc_cb, /*register_cb=*/nullptr, prof_free_cb, output_prefix_, - dump_args_level_ + dump_args_level_, dump_args_payload_filter_ ); if (rc != 0) { return rc; diff --git a/src/a5/platform/sim/host/device_runner.cpp b/src/a5/platform/sim/host/device_runner.cpp index 2acdb05f2..26c12a763 100644 --- a/src/a5/platform/sim/host/device_runner.cpp +++ b/src/a5/platform/sim/host/device_runner.cpp @@ -703,7 +703,7 @@ int DeviceRunner::init_args_dump(Runtime &runtime, int device_id) { int rc = dump_collector_.initialize( num_dump_threads, device_id, prof_alloc_cb, /*register_cb=*/nullptr, prof_free_cb, output_prefix_, - dump_args_level_ + dump_args_level_, dump_args_payload_filter_ ); if (rc != 0) { return rc; diff --git a/src/common/platform/include/common/args_dump.h b/src/common/platform/include/common/args_dump.h index 7af0cec9f..51055373a 100644 --- a/src/common/platform/include/common/args_dump.h +++ b/src/common/platform/include/common/args_dump.h @@ -45,6 +45,7 @@ #include "common/dfx_backpressure_device.h" #include "common/platform_config.h" +#include "args_dump_config.h" // ============================================================================= // Constants @@ -91,6 +92,10 @@ constexpr uint8_t ARGS_DUMP_RECORD_FLAG_ARG_INDEX_AMBIGUOUS = 1u << 0; // can't reference it here (platform layer doesn't include the tmap+ring runtime // header), so a static_assert in dump_args_for_task ties the two together. constexpr uint8_t ARGS_DUMP_MAX_FUNC_IDS = 3; +static_assert( + ARGS_DUMP_MAX_FUNC_IDS == DUMP_ARGS_PAYLOAD_MAX_FUNC_IDS, + "args dump records and payload filters must support the same func-id count" +); // ============================================================================= // ArgsDumpRecord - Single Args Dump Entry (128B = 2 cache lines) @@ -245,7 +250,7 @@ enum class DumpArgsLevel : uint32_t { OFF = 0, // no dump PARTIAL = 1, // only args marked with Arg::dump(...) FULL = 2, // every task's tensor/scalar I/O (JSON manifest + BIN payload) - FULL_JSON_ONLY = 3, // every task's tensor/scalar metadata to JSON; no BIN + FULL_JSON_ONLY = 3, // every task's metadata; optional filtered tensor payload }; struct DumpDataHeader { @@ -260,6 +265,7 @@ struct DumpDataHeader { uint64_t arena_size_per_thread; uint32_t magic; uint32_t dump_args_level; // DumpArgsLevel: 0=off, 1=partial, 2=full, 3=full_json_only + DumpArgsPayloadFilter payload_filter; // DFX backpressure coordination (unified across all DFX subsystems). DfxBackpressureHeader backpressure; } __attribute__((aligned(64))); diff --git a/src/common/platform/include/host/args_dump_collector.h b/src/common/platform/include/host/args_dump_collector.h index 155cc53e9..761885729 100644 --- a/src/common/platform/include/host/args_dump_collector.h +++ b/src/common/platform/include/host/args_dump_collector.h @@ -218,15 +218,19 @@ class ArgsDumpCollector : public profiling_common::ProfilerBase write_queue_; std::atomic writer_done_{false}; - // Resolved dump level; FULL_JSON_ONLY suppresses the .bin file entirely. + // Resolved dump level; FULL_JSON_ONLY suppresses .bin unless a payload + // filter is enabled. DumpArgsLevel dump_args_level_{DumpArgsLevel::OFF}; + DumpArgsPayloadFilter payload_filter_{}; // Output directory and single binary file std::filesystem::path run_dir_; diff --git a/src/common/platform/onboard/host/device_runner_base.cpp b/src/common/platform/onboard/host/device_runner_base.cpp index 1f92a8984..f5a9d1270 100644 --- a/src/common/platform/onboard/host/device_runner_base.cpp +++ b/src/common/platform/onboard/host/device_runner_base.cpp @@ -993,6 +993,7 @@ extern "C" __attribute__((weak)) int prewarm_config_impl( void DeviceRunnerBase::apply_call_config(const CallConfig &config) { set_l2_swimlane_enabled(config.enable_l2_swimlane); set_dump_args_enabled(config.enable_dump_args); + set_dump_args_payload_filter(config.dump_args_payload_filter); set_pmu_enabled(config.enable_pmu); // Virtual: a2a3 and a5 wire through to their enable_dep_gen_; an arch // without dep_gen falls through to the base no-op. diff --git a/src/common/platform/onboard/host/device_runner_base.h b/src/common/platform/onboard/host/device_runner_base.h index d34b97593..1c12c95b6 100644 --- a/src/common/platform/onboard/host/device_runner_base.h +++ b/src/common/platform/onboard/host/device_runner_base.h @@ -575,6 +575,7 @@ class DeviceRunnerBase { dump_args_level_ = static_cast(level); enable_dump_args_ = (dump_args_level_ != DumpArgsLevel::OFF); } + void set_dump_args_payload_filter(const DumpArgsPayloadFilter &filter) { dump_args_payload_filter_ = filter; } void set_pmu_enabled(int enable_pmu) { enable_pmu_ = (enable_pmu > 0); pmu_event_type_ = resolve_pmu_event_type(enable_pmu); @@ -1023,6 +1024,7 @@ class DeviceRunnerBase { bool enable_l2_swimlane_{false}; bool enable_dump_args_{false}; DumpArgsLevel dump_args_level_{DumpArgsLevel::OFF}; // resolved from set_dump_args_enabled() + DumpArgsPayloadFilter dump_args_payload_filter_{}; bool enable_pmu_{false}; bool enable_scope_stats_{false}; L2SwimlaneLevel l2_swimlane_level_{L2SwimlaneLevel::DISABLED}; // resolved from set_l2_swimlane_enabled() diff --git a/src/common/platform/shared/aicpu/args_dump_aicpu.cpp b/src/common/platform/shared/aicpu/args_dump_aicpu.cpp index bc857e574..0283cd5df 100644 --- a/src/common/platform/shared/aicpu/args_dump_aicpu.cpp +++ b/src/common/platform/shared/aicpu/args_dump_aicpu.cpp @@ -54,7 +54,7 @@ static constexpr uint64_t kDumpQueueBackpressureWaitCycles = PLATFORM_DFX_BACKPR static bool g_enable_dump_args = false; // Dump level latched from the header in dump_args_init(). The selective -// (PARTIAL) and json-only (FULL_JSON_ONLY) modes are derived from it rather +// (PARTIAL) and metadata-first (FULL_JSON_ONLY) modes are derived from it rather // than tracked as separate flags — mirrors g_l2_swimlane_level. static DumpArgsLevel g_dump_args_level = DumpArgsLevel::OFF; @@ -543,7 +543,8 @@ void dump_args_init(int num_dump_threads) { // Latch dump level from the host-written header before any task is dumped. // PARTIAL → selective (only Arg::dump()-marked args); FULL → every task; - // FULL_JSON_ONLY → every task, metadata only (no payload copied into arena). + // FULL_JSON_ONLY → every task's metadata; payload is copied only when the + // optional task+slot filter matches. g_dump_args_level = static_cast(s_dump_header->dump_args_level); LOG_INFO("Initializing args dump for %d threads", num_dump_threads); @@ -574,6 +575,45 @@ void dump_args_init(int num_dump_threads) { memset(s_buffers_flushed, 0, sizeof(s_buffers_flushed)); } +static bool payload_filter_matches(const ArgsDumpInfo &info) { + if (s_dump_header == nullptr) { + return false; + } + const DumpArgsPayloadFilter &filter = s_dump_header->payload_filter; + if (!filter.enabled() || info.arg_index >= DUMP_ARGS_PAYLOAD_ARG_MASK_BITS || + (filter.arg_mask & (uint64_t{1} << info.arg_index)) == 0) { + return false; + } + if (info.func_count <= 0 || info.func_count > ARGS_DUMP_MAX_FUNC_IDS) { + return false; + } + + uint32_t active_unique_count = 0; + for (int32_t i = 0; i < info.func_count; ++i) { + bool seen = false; + for (int32_t j = 0; j < i; ++j) { + if (info.func_ids[j] == info.func_ids[i]) { + seen = true; + break; + } + } + if (seen) continue; + active_unique_count++; + + bool selected = false; + for (uint32_t j = 0; j < filter.func_count; ++j) { + if (filter.func_ids[j] == info.func_ids[i]) { + selected = true; + break; + } + } + if (!selected) { + return false; + } + } + return active_unique_count == filter.func_count; +} + int dump_arg_record(int thread_idx, const ArgsDumpInfo &info) { if (s_dump_header == nullptr) { return -1; @@ -617,10 +657,9 @@ int dump_arg_record(int thread_idx, const ArgsDumpInfo &info) { bool truncated = false; bool is_contiguous = dump_arg_is_contiguous(info); bool is_scalar = kind == ArgsDumpKind::SCALAR; + bool capture_level3_payload = g_dump_args_level == DumpArgsLevel::FULL_JSON_ONLY && payload_filter_matches(info); - if (is_scalar || g_dump_args_level == DumpArgsLevel::FULL_JSON_ONLY) { - // JSON-only level captures full metadata but no payload, so the - // record carries shape/dtype/strides with payload_size == 0. + if (is_scalar || (g_dump_args_level == DumpArgsLevel::FULL_JSON_ONLY && !capture_level3_payload)) { copy_bytes = 0; } else if (bytes > state->arena_size) { // Tensor larger than entire arena — copy a partial sample diff --git a/src/common/platform/shared/host/args_dump_collector.cpp b/src/common/platform/shared/host/args_dump_collector.cpp index 3290f8820..8da80c958 100644 --- a/src/common/platform/shared/host/args_dump_collector.cpp +++ b/src/common/platform/shared/host/args_dump_collector.cpp @@ -91,7 +91,8 @@ void ArgsDumpCollector::merge_collector_shards() { int ArgsDumpCollector::initialize( int num_dump_threads, int device_id, const DumpAllocCallback &alloc_cb, DumpRegisterCallback register_cb, - const DumpFreeCallback &free_cb, const std::string &output_prefix, DumpArgsLevel dump_args_level + const DumpFreeCallback &free_cb, const std::string &output_prefix, DumpArgsLevel dump_args_level, + const DumpArgsPayloadFilter &payload_filter ) { if (shm_host_ != nullptr) { LOG_ERROR("ArgsDumpCollector already initialized"); @@ -112,6 +113,7 @@ int ArgsDumpCollector::initialize( num_dump_threads_ = num_dump_threads; output_prefix_ = output_prefix; dump_args_level_ = dump_args_level; + payload_filter_ = payload_filter; reset_collector_shards(); total_dropped_record_count_.store(0, std::memory_order_relaxed); total_truncated_count_.store(0, std::memory_order_relaxed); @@ -150,6 +152,7 @@ int ArgsDumpCollector::initialize( header->num_dump_threads = static_cast(num_dump_threads); header->records_per_buffer = PLATFORM_DUMP_RECORDS_PER_BUFFER; header->dump_args_level = static_cast(dump_args_level); + header->payload_filter = payload_filter; uint64_t arena_size = calc_dump_arena_size(); header->arena_size_per_thread = arena_size; @@ -241,10 +244,9 @@ void ArgsDumpCollector::start_writer_thread_once() { std::string run_dir_name = "args_dump"; run_dir_ = std::filesystem::path(output_prefix_) / run_dir_name; std::filesystem::create_directories(run_dir_); - // FULL_JSON_ONLY captures no payload (device sets payload_size == 0), so - // there is nothing to stream — skip the .bin file rather than leaving a - // 0-byte artifact next to the manifest. - if (dump_args_level_ != DumpArgsLevel::FULL_JSON_ONLY) { + // Level 3 remains JSON-only unless a payload filter explicitly selects + // tensor slots. + if (dump_args_level_ != DumpArgsLevel::FULL_JSON_ONLY || payload_filter_.enabled()) { bin_file_.open(run_dir_ / "args.bin", std::ios::binary); } next_bin_offset_ = 0; @@ -696,6 +698,25 @@ int ArgsDumpCollector::export_dump_files() { json << " \"type\": \"logical_contiguous\",\n"; json << " \"byte_order\": \"little_endian\"\n"; json << " },\n"; + json << " \"dump_args_level\": " << static_cast(dump_args_level_) << ",\n"; + if (payload_filter_.enabled()) { + json << " \"payload_filter\": {\"func_id\": ["; + for (uint32_t i = 0; i < payload_filter_.func_count; ++i) { + if (i != 0) json << ", "; + json << payload_filter_.func_ids[i]; + } + json << "], \"arg_indices\": ["; + bool first_slot = true; + for (uint32_t slot = 0; slot < DUMP_ARGS_PAYLOAD_ARG_MASK_BITS; ++slot) { + if ((payload_filter_.arg_mask & (uint64_t{1} << slot)) == 0) continue; + if (!first_slot) json << ", "; + json << slot; + first_slot = false; + } + json << "]},\n"; + } else { + json << " \"payload_filter\": null,\n"; + } json << " \"total_args\": " << collected_.size() << ",\n"; json << " \"before_dispatch\": " << num_before_dispatch << ",\n"; json << " \"after_completion\": " << num_after_completion << ",\n"; @@ -705,7 +726,7 @@ int ArgsDumpCollector::export_dump_files() { json << " \"truncated_args\": " << total_truncated_count_.load(std::memory_order_relaxed) << ",\n"; json << " \"dropped_records\": " << total_dropped_record_count_.load(std::memory_order_relaxed) << ",\n"; json << " \"dropped_overwrite\": " << total_overwrite_count_.load(std::memory_order_relaxed) << ",\n"; - if (dump_args_level_ == DumpArgsLevel::FULL_JSON_ONLY) { + if (dump_args_level_ == DumpArgsLevel::FULL_JSON_ONLY && !payload_filter_.enabled()) { json << " \"bin_file\": null,\n"; } else { json << " \"bin_file\": \"args.bin\",\n"; diff --git a/src/common/platform/sim/host/device_runner_base.cpp b/src/common/platform/sim/host/device_runner_base.cpp index 217971929..3a98d0ef6 100644 --- a/src/common/platform/sim/host/device_runner_base.cpp +++ b/src/common/platform/sim/host/device_runner_base.cpp @@ -589,6 +589,7 @@ extern "C" __attribute__((weak)) int prewarm_config_impl( void SimDeviceRunnerBase::apply_call_config(const CallConfig &config) { set_l2_swimlane_enabled(config.enable_l2_swimlane); set_dump_args_enabled(config.enable_dump_args); + set_dump_args_payload_filter(config.dump_args_payload_filter); set_pmu_enabled(config.enable_pmu); // a2a3 and a5 override set_dep_gen_enabled; an arch without dep_gen no-ops. set_dep_gen_enabled(config.enable_dep_gen != 0); diff --git a/src/common/platform/sim/host/device_runner_base.h b/src/common/platform/sim/host/device_runner_base.h index 0f37d9f6d..27620e3f1 100644 --- a/src/common/platform/sim/host/device_runner_base.h +++ b/src/common/platform/sim/host/device_runner_base.h @@ -201,6 +201,7 @@ class SimDeviceRunnerBase { dump_args_level_ = static_cast(level); enable_dump_args_ = (dump_args_level_ != DumpArgsLevel::OFF); } + void set_dump_args_payload_filter(const DumpArgsPayloadFilter &filter) { dump_args_payload_filter_ = filter; } void set_pmu_enabled(int enable_pmu) { enable_pmu_ = (enable_pmu > 0); pmu_event_type_ = resolve_pmu_event_type(enable_pmu); @@ -379,6 +380,7 @@ class SimDeviceRunnerBase { bool enable_l2_swimlane_{false}; bool enable_dump_args_{false}; DumpArgsLevel dump_args_level_{DumpArgsLevel::OFF}; // resolved from set_dump_args_enabled() + DumpArgsPayloadFilter dump_args_payload_filter_{}; bool enable_pmu_{false}; bool enable_scope_stats_{false}; L2SwimlaneLevel l2_swimlane_level_{L2SwimlaneLevel::DISABLED}; // resolved from set_l2_swimlane_enabled() diff --git a/src/common/task_interface/args_dump_config.h b/src/common/task_interface/args_dump_config.h new file mode 100644 index 000000000..a9ed9a636 --- /dev/null +++ b/src/common/task_interface/args_dump_config.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) PyPTO Contributors. + * This program is free software, you can redistribute it and/or modify it under the terms and conditions of + * CANN Open Software License Agreement Version 2.0 (the "License"). + * Please refer to the License for details. You may not use this file except in compliance with the License. + * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. + * See LICENSE in the root of the software repository for the full text of the License. + * ----------------------------------------------------------------------------------------------------------- + */ + +#pragma once + +#include +#include +#include +#include + +inline constexpr uint32_t DUMP_ARGS_PAYLOAD_MAX_FUNC_IDS = 3; +inline constexpr uint32_t DUMP_ARGS_PAYLOAD_ARG_MASK_BITS = 64; + +struct DumpArgsPayloadFilter { + int32_t func_ids[DUMP_ARGS_PAYLOAD_MAX_FUNC_IDS] = {}; + uint32_t func_count = 0; + uint64_t arg_mask = 0; + + bool enabled() const noexcept { return func_count != 0; } + + void validate() const { + if (func_count > DUMP_ARGS_PAYLOAD_MAX_FUNC_IDS) { + throw std::invalid_argument("DumpArgsPayloadFilter: func_count must be in [0, 3]"); + } + if (!enabled()) { + if (arg_mask != 0) { + throw std::invalid_argument("DumpArgsPayloadFilter: arg_mask requires at least one func_id"); + } + return; + } + if (arg_mask == 0) { + throw std::invalid_argument("DumpArgsPayloadFilter: at least one payload slot is required"); + } + for (uint32_t i = 0; i < func_count; ++i) { + if (func_ids[i] < 0) { + throw std::invalid_argument("DumpArgsPayloadFilter: func_ids must be non-negative"); + } + for (uint32_t j = i + 1; j < func_count; ++j) { + if (func_ids[i] == func_ids[j]) { + throw std::invalid_argument("DumpArgsPayloadFilter: func_ids must be unique"); + } + } + } + } +}; + +static_assert( + std::is_trivially_copyable_v && std::is_standard_layout_v, + "DumpArgsPayloadFilter must remain a memcpy-safe wire type" +); +static_assert(sizeof(DumpArgsPayloadFilter) == 24, "DumpArgsPayloadFilter wire layout drift"); +static_assert(offsetof(DumpArgsPayloadFilter, func_ids) == 0, "DumpArgsPayloadFilter.func_ids offset drift"); +static_assert(offsetof(DumpArgsPayloadFilter, func_count) == 12, "DumpArgsPayloadFilter.func_count offset drift"); +static_assert(offsetof(DumpArgsPayloadFilter, arg_mask) == 16, "DumpArgsPayloadFilter.arg_mask offset drift"); diff --git a/src/common/task_interface/call_config.h b/src/common/task_interface/call_config.h index e61c38b3c..875ab040e 100644 --- a/src/common/task_interface/call_config.h +++ b/src/common/task_interface/call_config.h @@ -49,6 +49,8 @@ #include #include +#include "args_dump_config.h" + inline constexpr int RUNTIME_ENV_RING_COUNT = 4; inline constexpr int RUNTIME_ENV_FIELD_GROUPS = 3; // ring_task_window, ring_heap, ring_dep_pool inline constexpr int RUNTIME_ENV_UINT64_FIELD_COUNT = RUNTIME_ENV_FIELD_GROUPS * RUNTIME_ENV_RING_COUNT; @@ -116,6 +118,7 @@ struct CallConfig { int32_t enable_dep_gen = 0; int32_t enable_scope_stats = 0; // writes /scope_stats/scope_stats.jsonl RuntimeEnv runtime_env; // per-task PTO2_RING_* overrides + DumpArgsPayloadFilter dump_args_payload_filter; char output_prefix[1024] = {}; bool diagnostics_any() const noexcept { @@ -138,11 +141,16 @@ struct CallConfig { ); } runtime_env.validate(); + dump_args_payload_filter.validate(); + if (dump_args_payload_filter.enabled() && enable_dump_args != 3) { + throw std::invalid_argument("CallConfig: dump_args_payload_filter requires enable_dump_args=3"); + } } }; #pragma pack(pop) static_assert(sizeof(RuntimeEnv) == RUNTIME_ENV_UINT64_FIELD_COUNT * sizeof(uint64_t), "RuntimeEnv wire layout drift"); static_assert( - sizeof(CallConfig) == 6 * sizeof(int32_t) + RUNTIME_ENV_UINT64_FIELD_COUNT * sizeof(uint64_t) + 1024, + sizeof(CallConfig) == + 6 * sizeof(int32_t) + RUNTIME_ENV_UINT64_FIELD_COUNT * sizeof(uint64_t) + sizeof(DumpArgsPayloadFilter) + 1024, "CallConfig wire layout drift" ); diff --git a/tests/st/a2a3/host_build_graph/prepared_callable/test_prepared_callable.py b/tests/st/a2a3/host_build_graph/prepared_callable/test_prepared_callable.py index 259f96a97..6bebcbc81 100644 --- a/tests/st/a2a3/host_build_graph/prepared_callable/test_prepared_callable.py +++ b/tests/st/a2a3/host_build_graph/prepared_callable/test_prepared_callable.py @@ -127,13 +127,23 @@ def _run_and_validate_l2( # noqa: PLR0913 enable_pmu=0, enable_dep_gen=False, enable_scope_stats=False, + dump_args_payload_filter=None, output_prefix="", ): params = case.get("params", {}) config_dict = case.get("config", {}) orch_sig = self.CALLABLE.get("orchestration", {}).get("signature", []) - config = self._build_config(config_dict) + config = self._build_config( + config_dict, + enable_l2_swimlane=enable_l2_swimlane, + enable_dump_args=enable_dump_args, + enable_pmu=enable_pmu, + enable_dep_gen=enable_dep_gen, + enable_scope_stats=enable_scope_stats, + dump_args_payload_filter=dump_args_payload_filter, + output_prefix=output_prefix, + ) chip_worker = self._chip_worker(worker) chip_worker._register_callable_at_slot(_SLOT_PRIMARY, callable_obj) diff --git a/tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py b/tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py index 24c61770e..663877e6d 100644 --- a/tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py +++ b/tests/st/a2a3/tensormap_and_ringbuffer/dfx/args_dump/test_args_dump.py @@ -18,6 +18,7 @@ """ import json +import struct import subprocess import sys import time @@ -119,9 +120,16 @@ def test_run(self, st_platform, st_worker, request): data = json.load(f) bin_name = data.get("bin_file") tensors = data.get("args", []) + payload_func_ids_spec = request.config.getoption("--dump-args-payload-func-id", default=None) + payload_func_ids = ( + sorted({int(value) for value in payload_func_ids_spec.split(",")}) if payload_func_ids_spec else [] + ) + payload_slots = sorted(set(request.config.getoption("--dump-args-payload-slot", default=[]))) assert tensors, f"args_dump.json has no entries: {data}" - if level == 3: + assert data.get("dump_args_level") == level + if level == 3 and not payload_slots: # full_json_only: metadata only, no payload and no .bin file. + assert data.get("payload_filter") is None assert bin_name is None, f"level 3 manifest should have bin_file=null: {data}" assert not (dump_dir / "args.bin").exists(), "level 3 must not write args.bin" assert all(t.get("bin_size") == 0 for t in tensors), tensors @@ -130,6 +138,33 @@ def test_run(self, st_platform, st_worker, request): bin_path = dump_dir / bin_name assert bin_path.exists(), f"manifest names bin_file={bin_name!r} but {bin_path} not found" assert bin_path.stat().st_size > 0, "args.bin is empty" + if level == 3: + assert data.get("payload_filter") == { + "func_id": payload_func_ids, + "arg_indices": payload_slots, + } + for entry in tensors: + selected_tensor = ( + entry.get("kind") == "tensor" + and set(entry.get("func_id", [])) == set(payload_func_ids) + and entry.get("arg_index") in payload_slots + ) + assert (entry.get("bin_size", 0) > 0) == selected_tensor, entry + if payload_func_ids == [0] and 0 in payload_slots: + entry = min( + ( + entry + for entry in tensors + if entry.get("stage") == "before_dispatch" + and entry.get("func_id") == [0] + and entry.get("arg_index") == 0 + ), + key=lambda entry: int(entry["task_id"], 16), + ) + with bin_path.open("rb") as payload_file: + payload_file.seek(entry["bin_offset"]) + payload = payload_file.read(entry["bin_size"]) + assert payload == struct.pack("magic = ARGS_DUMP_MAGIC; + hdr->dump_args_level = static_cast(DumpArgsLevel::FULL_JSON_ONLY); + hdr->num_dump_threads = 1; + hdr->payload_filter.func_ids[0] = 11; + hdr->payload_filter.func_ids[1] = 12; + hdr->payload_filter.func_count = 2; + hdr->payload_filter.arg_mask = uint64_t{1} << 6; + + DumpBufferState *state = get_dump_buffer_state(dump_mem, 0); + state->free_queue.buffer_ptrs[0] = reinterpret_cast(&meta_buf); + state->free_queue.tail = 1; + state->arena_base = reinterpret_cast(arena); + state->arena_size = kArenaSize; + + set_platform_dump_base(reinterpret_cast(dump_mem)); + dump_args_init(1); + + ArgsDumpInfo info = {}; + info.task_id = 0xCAFE; + info.role = ArgsDumpRole::INPUT; + info.stage = ArgsDumpStage::BEFORE_DISPATCH; + info.dtype = static_cast(DataType::UINT64); + info.ndims = 1; + info.arg_index = 6; + info.buffer_addr = reinterpret_cast(src_data); + info.shapes[0] = 4; + info.strides[0] = 1; + info.kind = static_cast(ArgsDumpKind::TENSOR); + info.func_count = 3; + info.func_ids[0] = 11; + info.func_ids[1] = 12; + info.func_ids[2] = 12; + + ASSERT_EQ(dump_arg_record(0, info), 0); + EXPECT_EQ(meta_buf.records[0].payload_size, sizeof(src_data)); + + info.arg_index = 7; + ASSERT_EQ(dump_arg_record(0, info), 0); + EXPECT_EQ(meta_buf.records[1].payload_size, 0u); + + info.arg_index = 6; + info.func_count = 2; + info.func_ids[0] = 11; + info.func_ids[1] = 13; + ASSERT_EQ(dump_arg_record(0, info), 0); + EXPECT_EQ(meta_buf.records[2].payload_size, 0u); + + info.func_ids[1] = 12; + info.kind = static_cast(ArgsDumpKind::SCALAR); + info.scalar_value = 99; + ASSERT_EQ(dump_arg_record(0, info), 0); + EXPECT_EQ(meta_buf.records[3].payload_size, 0u); + EXPECT_EQ(meta_buf.records[3].scalar_value, 99u); + + EXPECT_EQ(state->arena_write_offset, sizeof(src_data)); + EXPECT_EQ(meta_buf.count, 4u); + EXPECT_EQ(memcmp(arena, src_data, sizeof(src_data)), 0); + set_platform_dump_base(0); +} diff --git a/tests/ut/cpp/common/test_args_dump_collector.cpp b/tests/ut/cpp/common/test_args_dump_collector.cpp index 0dbcb4808..063819d41 100644 --- a/tests/ut/cpp/common/test_args_dump_collector.cpp +++ b/tests/ut/cpp/common/test_args_dump_collector.cpp @@ -89,6 +89,9 @@ TEST(ArgsDumpCollectorTest, MergesConcurrentShardRecordsIntoManifest) { std::ifstream manifest_file(manifest_path); ASSERT_TRUE(manifest_file.is_open()); const std::string manifest{std::istreambuf_iterator(manifest_file), std::istreambuf_iterator()}; + EXPECT_NE(manifest.find("\"dump_args_level\": 3"), std::string::npos); + EXPECT_NE(manifest.find("\"payload_filter\": null"), std::string::npos); + EXPECT_NE(manifest.find("\"bin_file\": null"), std::string::npos); EXPECT_NE(manifest.find("\"total_args\": " + std::to_string(kShardCount * kRecordsPerShard)), std::string::npos); collector.finalize(nullptr, test_free); diff --git a/tests/ut/cpp/types/test_call_config.cpp b/tests/ut/cpp/types/test_call_config.cpp index 6778d1362..5855c4695 100644 --- a/tests/ut/cpp/types/test_call_config.cpp +++ b/tests/ut/cpp/types/test_call_config.cpp @@ -10,6 +10,7 @@ */ #include +#include #include #include @@ -19,7 +20,10 @@ // Wire contract: parent and forked child move CallConfig with one memcpy. TEST(CallConfig, WireLayoutMatchesConstant) { EXPECT_EQ(sizeof(RuntimeEnv), RUNTIME_ENV_UINT64_FIELD_COUNT * sizeof(uint64_t)); - EXPECT_EQ(sizeof(CallConfig), 6 * sizeof(int32_t) + RUNTIME_ENV_UINT64_FIELD_COUNT * sizeof(uint64_t) + 1024); + EXPECT_EQ( + sizeof(CallConfig), + 6 * sizeof(int32_t) + RUNTIME_ENV_UINT64_FIELD_COUNT * sizeof(uint64_t) + sizeof(DumpArgsPayloadFilter) + 1024 + ); } TEST(CallConfig, RuntimeEnvDefaultsAreUnset) { @@ -87,3 +91,25 @@ TEST(CallConfig, RejectsInvalidValuesAtNonZeroRing) { cfg.runtime_env.ring_dep_pool[1] = static_cast(INT32_MAX) + 1; EXPECT_THROW(cfg.validate(), std::invalid_argument); } + +TEST(CallConfig, SelectivePayloadRequiresLevel3AndValidSelector) { + CallConfig cfg; + cfg.enable_dump_args = 3; + std::strcpy(cfg.output_prefix, "/tmp/out"); + cfg.dump_args_payload_filter.func_ids[0] = 11; + cfg.dump_args_payload_filter.func_ids[1] = 12; + cfg.dump_args_payload_filter.func_count = 2; + cfg.dump_args_payload_filter.arg_mask = (uint64_t{1} << 6) | (uint64_t{1} << 17); + EXPECT_NO_THROW(cfg.validate()); + + cfg.enable_dump_args = 2; + EXPECT_THROW(cfg.validate(), std::invalid_argument); + + cfg.enable_dump_args = 3; + cfg.dump_args_payload_filter.arg_mask = 0; + EXPECT_THROW(cfg.validate(), std::invalid_argument); + + cfg.dump_args_payload_filter.arg_mask = uint64_t{1} << 6; + cfg.dump_args_payload_filter.func_ids[1] = 11; + EXPECT_THROW(cfg.validate(), std::invalid_argument); +} diff --git a/tests/ut/py/test_chip_worker.py b/tests/ut/py/test_chip_worker.py index c27df0893..c998855c5 100644 --- a/tests/ut/py/test_chip_worker.py +++ b/tests/ut/py/test_chip_worker.py @@ -9,7 +9,12 @@ """Tests for CallConfig and ChipWorker state machine.""" import pytest -from _task_interface import CallConfig, RuntimeEnv, _ChipWorker # pyright: ignore[reportMissingImports] +from _task_interface import ( # pyright: ignore[reportMissingImports] + CallConfig, + DumpArgsPayloadFilter, + RuntimeEnv, + _ChipWorker, +) # ============================================================================ # CallConfig tests @@ -26,6 +31,8 @@ def test_defaults(self): assert config.enable_dump_args == 0 assert config.enable_pmu == 0 assert config.enable_dep_gen is False + assert config.dump_args_payload_filter.func_ids == [] + assert config.dump_args_payload_filter.slots == [] def test_setters(self): # enable_l2_swimlane accepts both an int perf_level (0-4) and a Python @@ -147,6 +154,30 @@ def test_runtime_env_per_ring_validate_rejects(self): with pytest.raises(ValueError): config.validate() + def test_dump_args_payload_filter_roundtrip_and_validation(self): + config = CallConfig() + config.enable_dump_args = 3 + config.dump_args_payload_filter.func_ids = [12, 11, 12] + config.dump_args_payload_filter.slots = [7, 6, 7] + config.output_prefix = "/tmp/out" + + assert config.dump_args_payload_filter.func_ids == [11, 12] + assert config.dump_args_payload_filter.slots == [6, 7] + config.validate() + assert "dump_args_payload_filter.func_ids=[11, 12]" in repr(config) + + config.enable_dump_args = 2 + with pytest.raises(ValueError, match="requires enable_dump_args=3"): + config.validate() + + empty = DumpArgsPayloadFilter() + empty.func_ids = [11] + with pytest.raises(ValueError, match="at least one payload slot"): + empty.validate() + + with pytest.raises(ValueError, match="at most 3"): + empty.func_ids = [1, 2, 3, 4] + config = CallConfig() config.runtime_env.ring_heap = [1024, 512, 2048, 4096] # 512 below min 1024 with pytest.raises(ValueError): @@ -296,7 +327,7 @@ def test_public_wrapper_rejects_raw_slot_run(self): worker = ChipWorker() with pytest.raises(TypeError, match="CallableHandle returned by ChipWorker.register_callable"): - worker.run(0, ChipStorageTaskArgs(), CallConfig()) + worker.run(0, ChipStorageTaskArgs(), CallConfig()) # pyright: ignore[reportArgumentType] # ============================================================================ @@ -319,13 +350,15 @@ def test_config_roundtrip(self): cfg = CallConfig() cfg.aicpu_thread_num = 2 cfg.enable_l2_swimlane = 3 - cfg.enable_dump_args = 2 + cfg.enable_dump_args = 3 cfg.enable_pmu = 5 cfg.enable_dep_gen = True cfg.enable_scope_stats = True cfg.runtime_env.ring_task_window = [16, 32, 128, 256] cfg.runtime_env.ring_heap = [1024, 2048, 4096, 8192] cfg.runtime_env.ring_dep_pool = [64, 128, 256, 512] + cfg.dump_args_payload_filter.func_ids = [11, 12] + cfg.dump_args_payload_filter.slots = [6, 17] cfg.output_prefix = "/tmp/out" buf = bytearray(_OFF_CONFIG + _CFG_FMT.size) @@ -341,17 +374,24 @@ def test_config_roundtrip(self): *cfg.runtime_env.ring_task_window, *cfg.runtime_env.ring_heap, *cfg.runtime_env.ring_dep_pool, + 11, + 12, + 0, + 2, + (1 << 6) | (1 << 17), cfg.output_prefix.encode(), ) decoded = _read_config_from_mailbox(memoryview(buf)) assert decoded.aicpu_thread_num == 2 assert decoded.enable_l2_swimlane == 3 - assert decoded.enable_dump_args == 2 + assert decoded.enable_dump_args == 3 assert decoded.enable_pmu == 5 assert decoded.enable_dep_gen is True assert decoded.enable_scope_stats is True assert decoded.runtime_env.ring_task_window == [16, 32, 128, 256] assert decoded.runtime_env.ring_heap == [1024, 2048, 4096, 8192] assert decoded.runtime_env.ring_dep_pool == [64, 128, 256, 512] + assert decoded.dump_args_payload_filter.func_ids == [11, 12] + assert decoded.dump_args_payload_filter.slots == [6, 17] assert decoded.output_prefix == "/tmp/out" diff --git a/tests/ut/py/test_l0_swimlane.py b/tests/ut/py/test_l0_swimlane.py new file mode 100644 index 000000000..7d47c222a --- /dev/null +++ b/tests/ut/py/test_l0_swimlane.py @@ -0,0 +1,290 @@ +#!/usr/bin/env python3 +# Copyright (c) PyPTO Contributors. +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of +# CANN Open Software License Agreement Version 2.0 (the "License"). +# Please refer to the License for details. You may not use this file except in compliance with the License. +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. +# See LICENSE in the root of the software repository for the full text of the License. +# ----------------------------------------------------------------------------------------------------------- +"""Contract tests for payload restoration in simpler_setup.tools.l0_swimlane.""" + +import json +import struct +import sys +from pathlib import Path +from typing import Optional + +import pytest + +from simpler_setup.tools import l0_swimlane + + +def _tensor_record( # noqa: PLR0913 + *, + slot=6, + dtype="UINT8", + shape=None, + strides=None, + start_offset=0, + stage="before_dispatch", + role="input", + bin_offset=0, + bin_size=4, + truncated=False, + overwritten=False, +): + shape = [4] if shape is None else shape + strides = [1] if strides is None else strides + return { + "task_id": "0x10", + "func_id": [11, 12, 12], + "role": role, + "stage": stage, + "arg_index": slot, + "kind": "tensor", + "dtype": dtype, + "shape": shape, + "strides": strides, + "start_offset": start_offset, + "bin_offset": bin_offset, + "bin_size": bin_size, + "truncated": truncated, + "overwritten": overwritten, + } + + +def _write_dump( + tmp_path: Path, + records, + payload=b"", + *, + bin_file: Optional[str] = "args.bin", + bin_format=None, + payload_filter=None, +): + dump_dir = tmp_path / "args_dump" + dump_dir.mkdir() + manifest = dump_dir / "args_dump.json" + manifest.write_text( + json.dumps( + { + "bin_format": bin_format + or { + "type": "logical_contiguous", + "byte_order": "little_endian", + }, + "bin_file": bin_file, + "dump_args_level": 3, + "payload_filter": payload_filter, + "args": records, + } + ) + ) + if bin_file is not None: + (dump_dir / bin_file).write_bytes(payload) + return manifest + + +def _reconstruct(manifest): + _, tensor_count, kargs, _ = l0_swimlane.reconstruct_task_args(manifest, [11, 12]) + return tensor_count, kargs + + +def test_restore_arg_loads_before_dispatch_payload_and_emits_memcpy(tmp_path): + before = _tensor_record(bin_offset=0, bin_size=4) + after = _tensor_record( + stage="after_completion", + role="inout", + bin_offset=4, + bin_size=4, + ) + manifest = _write_dump(tmp_path, [after, before], b"\x01\x02\x03\x04\x09\x09\x09\x09") + + tensor_count, kargs = _reconstruct(manifest) + l0_swimlane.restore_arg_payloads(manifest, kargs, [6]) + + assert kargs[0]["restore_data"] == b"\x01\x02\x03\x04" + host = l0_swimlane.emit_replay_host(tensor_count, kargs) + assert "static const unsigned char hbuf0[]" in host + assert "0x01, 0x02, 0x03, 0x04" in host + assert "aclrtMemcpy(d_t0, t0Bytes, hbuf0, sizeof(hbuf0)" in host + assert "aclrtMemset(d_t0" not in host + + +def test_restore_arg_scatters_logical_payload_into_strided_physical_view(tmp_path): + logical = struct.pack("<4H", 10, 20, 30, 40) + record = _tensor_record( + dtype="UINT16", + shape=[2, 2], + strides=[3, 1], + start_offset=1, + bin_size=len(logical), + ) + manifest = _write_dump(tmp_path, [record], logical) + + _, kargs = _reconstruct(manifest) + l0_swimlane.restore_arg_payloads(manifest, kargs, [6]) + + assert struct.unpack("<6H", kargs[0]["restore_data"]) == (0, 10, 20, 0, 30, 40) + + +@pytest.mark.parametrize( + ("record_updates", "message"), + [ + ({"stage": "after_completion", "role": "output"}, "no before_dispatch payload"), + ({"truncated": True}, "payload is truncated"), + ({"overwritten": True}, "payload was overwritten"), + ({"bin_size": 3}, "does not match logical tensor size"), + ], +) +def test_restore_arg_rejects_unusable_tensor_records(tmp_path, record_updates, message): + record = _tensor_record(**record_updates) + manifest = _write_dump(tmp_path, [record], b"\x01\x02\x03\x04") + _, kargs = _reconstruct(manifest) + + with pytest.raises(ValueError, match=message): + l0_swimlane.restore_arg_payloads(manifest, kargs, [6]) + + +def test_restore_arg_rejects_json_only_dump(tmp_path): + record = _tensor_record(bin_size=0) + manifest = _write_dump(tmp_path, [record], bin_file=None) + _, kargs = _reconstruct(manifest) + + with pytest.raises(ValueError, match="no bin_file"): + l0_swimlane.restore_arg_payloads(manifest, kargs, [6]) + + +def test_restore_arg_rejects_missing_slot(tmp_path): + manifest = _write_dump(tmp_path, [_tensor_record()], b"\x01\x02\x03\x04") + _, kargs = _reconstruct(manifest) + + with pytest.raises(ValueError, match="slot 7 is not an arg"): + l0_swimlane.restore_arg_payloads(manifest, kargs, [7]) + + +def test_restore_arg_rejects_unknown_bin_format(tmp_path): + manifest = _write_dump( + tmp_path, + [_tensor_record()], + b"\x01\x02\x03\x04", + bin_format={"type": "physical", "byte_order": "little_endian"}, + ) + _, kargs = _reconstruct(manifest) + + with pytest.raises(ValueError, match="type=logical_contiguous"): + l0_swimlane.restore_arg_payloads(manifest, kargs, [6]) + + +def test_restore_arg_rejects_scalar_slot(tmp_path): + scalar = { + "task_id": "0x10", + "func_id": [11, 12, 12], + "role": "scalar", + "stage": "before_dispatch", + "arg_index": 17, + "kind": "scalar", + "dtype": "INT64", + "shape": [], + "strides": [], + "start_offset": 0, + "value": 9, + "bin_offset": 0, + "bin_size": 0, + } + manifest = _write_dump(tmp_path, [scalar], b"") + _, kargs = _reconstruct(manifest) + + with pytest.raises(ValueError, match="is a scalar"): + l0_swimlane.restore_arg_payloads(manifest, kargs, [17]) + + +def test_get_or_run_dump_uses_level3_and_forwards_payload_filter(tmp_path, monkeypatch): + commands = [] + monkeypatch.setattr(l0_swimlane, "PROJECT_ROOT", tmp_path) + + def fake_run(cmd, **_kwargs): + commands.append(cmd) + dump_dir = tmp_path / "outputs" / "run_3" / "args_dump" + dump_dir.mkdir(parents=True) + (dump_dir / "args_dump.json").write_text("{}") + + monkeypatch.setattr(l0_swimlane.subprocess, "run", fake_run) + manifest = l0_swimlane.get_or_run_dump( + tmp_path / "test_case.py", + "a2a3sim", + "sim", + None, + payload_func_ids=[11, 12], + payload_slots=[6, 7], + ) + + flag_index = commands[0].index("--dump-args") + assert commands[0][flag_index + 1] == "3" + func_index = commands[0].index("--dump-args-payload-func-id") + assert commands[0][func_index + 1] == "11,12" + assert commands[0].count("--dump-args-payload-slot") == 2 + assert commands[0][commands[0].index("--dump-args-payload-slot") + 1] == "6" + assert manifest.name == "args_dump.json" + + +def test_replay_cmake_includes_shared_platform_headers(): + cmake = l0_swimlane.emit_cmakelists( + "a2a3", + "test", + l0_swimlane.ARCH_CONFIG["a2a3"], + extra_include_dirs=["/sdk/asc/include"], + ) + + assert "${REPO_ROOT}/src/common/platform/include" in cmake + assert '"/sdk/asc/include"' in cmake + + +def test_run_collect_forwards_msprof_timeout_in_minutes(): + script = l0_swimlane.emit_run_collect( + l0_swimlane.ARCH_CONFIG["a2a3"], + "/pto-isa", + msprof_timeout=4, + ) + + assert "--timeout=4" in script + assert "__MSPROF_TIMEOUT__" not in script + + +def test_cli_rejects_restore_and_set_on_the_same_slot(monkeypatch, capsys): + monkeypatch.setattr( + sys, + "argv", + [ + "l0_swimlane", + "--test", + "unused.py", + "--func-id", + "11,12", + "--restore-arg", + "6", + "--set-arg", + "6=1", + ], + ) + + with pytest.raises(SystemExit, match="2"): + l0_swimlane.main() + + assert "cannot use both --restore-arg and --set-arg" in capsys.readouterr().err + + +def test_default_tensor_initialization_remains_zero_fill(): + tensor = { + "kind": "tensor", + "slot": 0, + "dtype": "UINT8", + "shape": [4], + "strides": [1], + "start_offset": 0, + } + + host = l0_swimlane.emit_replay_host(1, [tensor]) + + assert "aclrtMemset(d_t0, t0Bytes, 0, t0Bytes)" in host diff --git a/tests/ut/py/test_scene_test_dump_args.py b/tests/ut/py/test_scene_test_dump_args.py new file mode 100644 index 000000000..1e69d90b1 --- /dev/null +++ b/tests/ut/py/test_scene_test_dump_args.py @@ -0,0 +1,52 @@ +# Copyright (c) PyPTO Contributors. +# This program is free software, you can redistribute it and/or modify it under the terms and conditions of +# CANN Open Software License Agreement Version 2.0 (the "License"). +# Please refer to the License for details. You may not use this file except in compliance with the License. +# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, +# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. +# See LICENSE in the root of the software repository for the full text of the License. +# ----------------------------------------------------------------------------------------------------------- +"""Tests for SceneTest's selective args-payload CLI contract.""" + +import pytest + +from simpler_setup.scene_test import ( + _resolve_dump_args_payload_filter, + _validate_dump_args_payload_scene_level, +) + + +def test_resolve_dump_args_payload_filter_canonicalizes_values(): + assert _resolve_dump_args_payload_filter(3, "12,11,12", [7, 6, 7]) == { + "func_ids": [11, 12], + "slots": [6, 7], + } + + +@pytest.mark.parametrize( + ("level", "func_ids", "slots", "message"), + [ + (2, "11,12", [6], "requires --dump-args 3"), + (3, None, [6], "must be provided together"), + (3, "11,12", [], "must be provided together"), + (3, "11,bad", [6], "comma-separated list of integers"), + (3, "1,2,3,4", [6], "1-3 non-negative unique ids"), + (3, "11,12", [64], r"must be in \[0, 63\]"), + ], +) +def test_resolve_dump_args_payload_filter_rejects_invalid_combinations(level, func_ids, slots, message): + with pytest.raises(ValueError, match=message): + _resolve_dump_args_payload_filter(level, func_ids, slots) + + +def test_resolve_dump_args_payload_filter_is_optional(): + assert _resolve_dump_args_payload_filter(0, None, []) is None + + +def test_selective_payload_filter_is_supported_for_l2(): + _validate_dump_args_payload_scene_level(2, {"func_ids": [11, 12], "slots": [6]}) + + +def test_selective_payload_filter_is_rejected_for_l3(): + with pytest.raises(NotImplementedError, match="L2 dump -> L0 replay only"): + _validate_dump_args_payload_scene_level(3, {"func_ids": [11, 12], "slots": [6]})