Skip to content

Merging devel to ze-validator-dev - #4

Closed
myrepo1 wants to merge 3 commits into
ze-validator-devfrom
devel
Closed

myrepo1 wants to merge 3 commits into
ze-validator-devfrom
devel

Conversation

@myrepo1

@myrepo1 myrepo1 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

TApplencourt and others added 3 commits July 14, 2026 11:17
* ze: host-read kernel timestamps for immediate command lists (fix O(N²))

The tracer baked a zeCommandListAppendQueryKernelTimestamps (QKT) per
profiled Append into the user's immediate command list. The L0 driver's
per-QKT cost is superlinear in the number of QKT ops simultaneously in
flight (signaled-but-not-synced), independent of queue ordering. Workloads
that signal faster than they sync grow that backlog without bound, so the
tracer overhead grew O(N²) over the run (measured: kynema_sgf/AMReX Solve
time 1.8s→4.3s over 10 steps under iprof; untraced flat at 0.69s).

Fix: for immediate cls, do not bake a device QKT. The prologue already
swaps the user's signal for our injected KERNEL_TIMESTAMP event `inj`, so
the kernel/copy signals `inj` directly and it carries the real kernel
timing. Append a barrier (wait=inj, sig=user_signal) to re-expose the
user's event, and at drain read the timing host-side via
zeEventQueryKernelTimestamp(inj). Because inj is tracer-owned it is never
reused by the user, so host-read is never overwritten — no reuse
detection needed. Regular cls are unchanged (they are replayed per
Execute, so host-read can't capture each Execute; their in-flight QKT
count is bounded by un-synced Executes, not total appends).

Also replaces the O(N) _cl_any_live slot scan with an O(1) n_live
counter (deletes _cl_any_live and the _ZE_FOREACH_SLOT macro).

Verified: reproducer per-sync ratio flat (was 1.6/2.1/3.2 at
N=32k/64k/128k → now ~1.03); correctness 58/58; benches pass; copy
engine host-read timestamps valid; real app step-10 overhead 6.2×→1.7×
with the O(N²) growth eliminated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ze: drop immediate-cl reset workaround (no longer needed, was residual O(N))

_imm_reset_if_drained raw-Reset the user's immediate command list every
time its in-flight backlog drained to zero. Its sole purpose was to
reclaim the L0 driver's per-AppendQueryKernelTimestamps storage, which
accumulated on a long-lived reused immediate cl. Immediate cls no longer
bake a device QKT (they host-read the injected event), so there is no
such storage to reclaim, and their slot bookkeeping is already fully
reclaimed by _slot_release at drain (events returned to the pool, empty
non-tail chunks freed).

The reset was pure cost: a full driver command-list reset on every
drain-to-empty. On the real app (kynema_sgf, 50 steps) removing it cut
~0.22s off every step AND collapsed the remaining linear creep from +32%
to +3.2% over the run (Solve 1.119->1.481 became 0.894->0.923; overhead
vs untraced now ~1.33x and flat).

The separate shadow-cl reset in _slot_drain (gated by s->sh) is kept —
regular copy-only cls still bake QKT on the shadow cl.

Verified: correctness 58/58, benches pass, reproducer flat, real app flat
across 50 steps.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ze: unify all command lists onto host-read timestamps; delete device QKT + shadow

Extends the immediate-cl host-read approach to REGULAR command lists too, so
ALL kernel/copy timing is captured by host-reading a tracer-owned injected
KERNEL_TIMESTAMP event (zeEventQueryKernelTimestamp at drain) instead of a
device-side zeCommandListAppendQueryKernelTimestamps (QKT).

Deletes, now unused:
- the per-(context,device) "shadow" command list (existed only to host QKT for
  copy queue groups, which reject QKT),
- the queue-group class cache and is_compute (only chose inline-vs-shadow),
- the device-written timestamp slab and per-slot offset,
- _append_inline_query / _shadow_append_query / _slot_publish.
Net ~ -330 lines.

Scheme (one uniform path):
- Append: prologue swaps the user's signal for our inj (kernel/copy signals
  inj); re-expose the user's signal on the user cl — in-order cls use a plain
  zeCommandListAppendSignalEvent (measured ~9% cheaper than a barrier: ~1500ns
  vs ~1640ns per op), out-of-order cls use zeCommandListAppendBarrier(wait=inj)
  for ordering. Then instantiate the slot.
- Drain: zeEventQueryKernelTimestamp(inj); stash under the user's event. Never
  reset inj (a replayed regular cl's next Execute overwrites it, last-value-wins
  like the old slab; resetting would hang a second drain on a never-re-signaled
  event). inj disposed at cl Reset/Destroy (regular) or _slot_release (imm).
- Execute epilogue stays one critical section (force-sync-prior + drain +
  re-instantiate + claim in_flight_q) so concurrent Executes observe in_flight_q
  atomically.

INVARIANT: the tracer only ever syncs/queries/resets ITS OWN inj event; the
user's event is read-only (we stash timing under it). The sole user-object sync
is the Execute epilogue's zeCommandQueueSynchronize on a QUEUE (never an event)
to drain a prior in-flight submission.

Verified on Intel Max 1550: correctness 58/58 (serial and bats --jobs 4), all
benches pass, the O(N^2) reproducer flat, reg_Event_09/10/11 and
multithreaded_01/02 correct, no hang. (The hang seen in an earlier build was
flakiness / an intermediate broken prologue-split, not this design.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ze: run Execute bookkeeping in the prologue; reset our injected event at drain

Moves all zeCommandQueueExecuteCommandLists tracer bookkeeping (force-sync the
prior in-flight instance + drain + re-instantiate slots + claim in_flight_q)
from the epilogue into the PROLOGUE, as one critical section, and resets our
injected event at drain instead of relying on the next replay to overwrite it.

Why the prologue: a regular cl is closed once and replayed; its injected
KERNEL_TIMESTAMP event is baked into the cl body and re-signaled by every
Execute. Draining the previous instance AFTER the next submit (the old
epilogue) races that re-signal — for back-to-back Executes with no user sync
between (reg_Event_09) the previous instance's timing was clobbered by the
next before we read it. Draining in the prologue reads and resets our event
BEFORE this submission re-signals it, so each replay yields its own timing.
The same prologue force-sync serializes a cl reused concurrently from another
thread (reg_Event_multithreaded_01), so nothing needs to run after submit.

Resetting our injected event at drain (rather than depending on re-signal
overwriting its value) removes reliance on undocumented driver behavior. It is
safe: a slot is only re-drained after being re-instantiated at a later Execute,
which re-signals the event first; the final drain of a slot is never followed
by another. We only ever reset OUR event, never the user's. Immediate cls reset
it via the pool recycle path (_put_ze_event); only kept-across-replay regular
slots need the explicit reset.

Also folds in review cleanup:
- rename _ze_slab_chunk/_ZE_SLAB_CHUNK_SLOTS -> _ze_slot_chunk/_ZE_CHUNK_SLOTS
  (no slab remains; the chunk is now purely a slot arena);
- in-order cls re-expose the user signal with zeCommandListAppendSignalEvent
  (measured ~9% cheaper than a barrier); out-of-order cls keep the barrier;
- drop the unused device/ordinal params from _on_create_command_list; use bool
  for is_immediate/is_in_order/live/has_kts (add <stdbool.h>);
- merge the reset/destroy chunk-reclaim duplication into _cl_reclaim_chunks;
- strip stale comments that described the removed QKT/shadow/slab machinery.

NOT yet hardware-verified (node lost mid-session): build is clean; the prior
commit is 58/58 + benches. Re-run on hardware, priority reg_Event_09 (distinct
per-replay timings), multithreaded_01/02, full suite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ze: reclaim emptied tail chunk at append (fix mem_persistent_cl leak)

On the event-sync drain path (_on_sync -> _ZE_SYNC_EVENT -> _slot_drain), an
empty slab chunk could leak on a long-lived reused immediate cl. A chunk is
freed by _slot_release only when a release drives n_held to 0 AND the chunk is
non-tail (the tail is guarded so new Appends keep landing on it). The one
transition that leaves an empty non-tail chunk is a chunk that drained empty
while it was still the tail and was later superseded by a new tail — nothing
revisited it.

Fix it at that single transition: when _cl_slot_append allocates a new chunk,
reclaim the old tail if it is already empty (n_held==0). O(1) on a path that
already allocates; every other empty non-tail chunk is still freed inline by
_slot_release the moment it drains. No separate sweep pass, and any future
partial-drain sync anchor gets the reclaim for free.

On bench/mem_persistent_cl (reused immediate cl synced via
zeEventHostSynchronize) this leaked ~582 B/round (116 KB, threshold 64 KB).

Also clang-format-18 the file (fixes pre-existing comment-alignment and
signature-wrapping violations).

Verified on hardware: repro 116464 -> 64 B (flat), flat across NR=100..800 in
every appends-per-round regime, suite 65/65.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ze: O(1) in-order predecessor via per-cl live-slot list (fix O(N^2) build)

_slot_instantiate found the in-order predecessor by scanning chunks
newest-to-oldest for the first still-live slot. On a long-lived in-order
immediate cl whose completed Appends stay referenced cross-cl (a downstream
slot in another cl waits on them, refs>0, so their chunk is not reclaimed),
that scan walks all the drained-but-pinned slots ahead of the new one — O(N)
per Append, O(N^2) to build the cl.

Maintain a per-cl doubly-linked list of live slots in append order
(cl_data->live_slots, _ze_slot.live_prev/live_next). The in-order predecessor
is its tail (O(1)); slots join at instantiate and leave at drain. Bulk teardown
(reset/destroy) drops the whole list by nulling the head — detached slots have
owner==NULL so their later drain skips the unlink. Also bounds _slot_drain's
in-order recursion depth.

Measured on hardware (immediate in-order cl, completed Appends pinned by an
un-synced second cl, LTTNG_UST_ZE_PROFILE direct):
  before: per-Append last/first-quartile ratio 2.61 at backlog 32000 (grows with N)
  after:  ratio 1.00, flat
Suite 66/66 (new bench pinned_pred_walk_scaling; verified it fails pre-fix).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ze: drain injected events on zeEvent/zeFenceQueryStatus, not just blocking sync

Apps that observe completion by polling zeEventQueryStatus / zeFenceQueryStatus
to ZE_RESULT_SUCCESS (never the blocking HostSynchronize) never triggered a
drain: the tracer only recycled injected events on the blocking sync anchors.
Result was an unbounded injected-event leak AND lost profiling — a slot that
never drains never emits its event_profiling_results tracepoint.

Hook both QueryStatus polls to the same drain anchor as their blocking
counterparts (SUCCESS return carries the same completion fact). Safe: _slot_drain
no-ops on already-drained slots under the state mutex, so repeated polls drain
once. Drop the stale "fence QueryStatus is unsafe to hook" note.

Also removes the zeEventPoolCreate/zeEventCreate prologues that forced
KERNEL_TIMESTAMP|HOST_VISIBLE on pools and HOST scope on events — timing comes
from our own injected event, the user's events stay as created.

Covered by ze tests inorder_imm_Event_10 (event) and ooo_reg_Event_15 (fence):
both fail pre-fix (0/100 and 99/100) and pass post-fix (100/100).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ze: trim tracer overhead and clean up helpers

Reduce per-Append and per-slot cost in the ZE tracer, plus a pass of
readability cleanup, with no change to traced behavior:

- Store the cl's context at create time and read it in the Append prologue,
  removing the per-Append zeCommandListGetContextHandle driver call (resolved
  via the existing cl hash lookup). Unregistered cls now skip cleanly instead
  of injecting an event and rolling it back.
- Move `owner` from every slot onto its chunk (all slots in a chunk share one
  cl): ~504 B saved per 64-slot chunk, and chunk detach becomes a single
  assignment instead of a per-slot loop.
- Event freelists are pure LIFO stacks: use utlist LL_ (drop the unused prev
  pointer) instead of DL_.
- Designated initializers for the property-dump structs (also fixes two
  previously-uninitialized ones); bool instead of int for local flags;
  rename _universal_record_append -> _record_append; collapse the dlopen
  branch; drop a duplicate include.
- Comment cleanup: describe observable behavior rather than tracer internals,
  drop stale references (shadow cls, QKT, test names) and de-duplicate the
  repeated invariant restatements.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ze: batch injected events into shared pools

zeEventPoolCreate is expensive, and the tracer created one pool per injected
event. Allocate pools with capacity _ZE_EVENT_POOL_CAP (64) instead and hand
out events by index, creating a new pool only when the current one fills. At
512 concurrent injected events this drops pool-creates from 512 to 8 (ceil
512/64); event create/recycle and every hot path are unchanged.

The shared pool is owned by the per-context entry (a list of pools for
teardown), not the wrapper, so the wrapper drops its event_pool field.
Events still recycle through the per-context freelist; pools live until the
context is destroyed.

Teardown respects the L0 rule that every event be destroyed before its pool:
context-destroy destroys all injected events (in-flight via the cl sweep's
_ZE_DISPOSE_WRAPPER, then the freelist) before destroying any pool. Rename
the ctx_dying flag to ctx_destroy and fix its now-stale comment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* ze: rework injected-event allocator, fix cl destroy gate, harden + document

Injected-event allocator (tracer_ze_helpers.include.c):
- delete the global struct-recycle pool; the per-context L0-event freelist
  is the only recycle. calloc/free the wrapper struct directly.
- rename to intent: _get_event/_put_event (API), _internal_* (file-local;
  not __ which the C standard reserves), _ZE_DISPOSE_RECYCLE/_DESTROY, chunk->slab.
- ZE_MUST on the teardown destroy calls; warn-once on zeEventPoolCreate failure.
- remove dead NULL guards proven unreachable by every caller
  (_event_state_del, _event_state_get_or_add, _slot_release, _record_append
  !inj, _slot_drain !s); annotate the one live !ev guard that survives.
- int->bool on the kts predicates.

zeCommandListDestroy gate _do_profile -> _do_state(), symmetric with the
create hooks: a memory-info-only config registered cls it never unregistered,
leaking the registration and colliding with handle-recycled creates.

Docs: top-of-file rationale + a "Timestamp profiling architecture" section in
README.md (ownership / counters / state-machine / teardown, with the detach
worked example). Comment editor pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Fix clang-format

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Thomas Applencourt <applenco@sunspot-uan-0002.head.cm.sunspot.alcf.anl.gov>
Co-authored-by: Thomas Applencourt <applenco@sunspot-uan-0001.head.cm.sunspot.alcf.anl.gov>
…argonne-lcf#517)

The MPI tracer shim is LD_PRELOADed as libmpi.so and dlopen()s the real
libmpi. It was opened with RTLD_LOCAL, which keeps the real library's
symbols out of the global scope. Any MPI symbol the shim does not itself
wrap (e.g. MPIX_GPU_query_support) then fails to resolve in the traced
application:

  ./app: symbol lookup error: ./app: undefined symbol: MPIX_GPU_query_support

Switch both dlopen() calls (LTTNG_UST_MPI_LIBMPI and the libmpi.so
fallback) to RTLD_GLOBAL, keeping RTLD_DEEPBIND. Our preloaded wrappers
still take precedence for wrapped symbols since they are earlier in the
global scope, and RTLD_DEEPBIND keeps the real lib's internal calls bound
to itself rather than re-entering our wrappers.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@myrepo1 myrepo1 closed this Jul 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants