|
1 | | -# Header Location |
| 1 | +# How to Update the Header |
| 2 | + |
| 3 | +## Header Location |
2 | 4 |
|
3 | 5 | - Standard: `https://github.com/argonne-lcf/level-zero-spec/tree/ddi_ver` |
4 | 6 | - Loader: `https://github.com/oneapi-src/level-zero/tags` |
@@ -165,3 +167,121 @@ PogrammableParamValueInfoExp>': uninitialized constant ZE::ZETMetricProgrammable |
165 | 167 | ``` |
166 | 168 | Update in `backends/ze/gen_ze_library.rb`, Module ZE. |
167 | 169 | TODO: `h2yaml` should be able to find all the constants defined. |
| 170 | + |
| 171 | +# Timestamp profiling architecture |
| 172 | + |
| 173 | +This documents the runtime bookkeeping in `tracer_ze_helpers.include.c` / |
| 174 | +`ze_model.rb` that measures how long each submitted command takes. |
| 175 | + |
| 176 | +## Why it needs bookkeeping at all |
| 177 | + |
| 178 | +The goal is to measure each command submitted to the GPU. The only way is to |
| 179 | +read a profiling event the command signals. The subtlety: users are free to |
| 180 | +re-signal, share, and reset their events. So: |
| 181 | + |
| 182 | +- **If the user passes no signal event**, we inject our own and signal it. |
| 183 | +- **If the user passes a signal event**, we swap in our injected event, then |
| 184 | + re-expose the user's event (a chained SignalEvent / Barrier) so their code |
| 185 | + still works. |
| 186 | + |
| 187 | +At synchronization time we host-read our injected event to get the real kernel |
| 188 | +timing. Two API facts stop this from being a simple per-event list: |
| 189 | + |
| 190 | +1. **Users sync more than events** — a whole command list, a queue, or a fence, |
| 191 | + not just `zeEventHostSynchronize`. So we must track, per command list, the |
| 192 | + set of outstanding profiled commands (and a live count, to know when a cl is |
| 193 | + fully drained). |
| 194 | +2. **A command signaled on one cl can be waited on from another** (cross-cl |
| 195 | + `phWaitEvents`). So the dependency chain spans command lists, and a command's |
| 196 | + storage must outlive its own cl being reset/destroyed while another cl still |
| 197 | + depends on it (the **detach** mechanism). |
| 198 | + |
| 199 | +Each profiled Append becomes a **slot** (injected event + dependency edges + |
| 200 | +refcount). Slots are grouped per cl in fixed-size **slabs** (so cl/queue/fence |
| 201 | +sync can find them) but linked by raw pointers across cls (for the dependency |
| 202 | +chain). |
| 203 | + |
| 204 | +## Ownership |
| 205 | + |
| 206 | +```mermaid |
| 207 | +graph TD |
| 208 | + cls["_ze_cls (global uthash)"] --> cld["cl_data"] |
| 209 | + cld -- slabs (utlist DL) --> s1["slab"] |
| 210 | + s1 --> s2["slab"] |
| 211 | + s2 --> s3["slab (tail)"] |
| 212 | + s1 -- "slots[64] inline" --> slot["slot"] |
| 213 | + cld -. "live_slots (in-order only)" .-> slot |
| 214 | + slot -- "slab (back-link)" --> s1 |
| 215 | + slot -- "preds[] (may cross cl)" --> other["slot in ANOTHER cl"] |
| 216 | + other -. "each edge: target->refs++" .-> slot |
| 217 | +``` |
| 218 | + |
| 219 | +Slabs grow by appending (never realloc), so a slot's address is stable for as |
| 220 | +long as it lives — required, because other slots hold raw pointers to it. |
| 221 | + |
| 222 | +**Why the slot -> slab back-link:** `_slot_drain` / `_slot_release` are entered |
| 223 | +holding only a bare slot pointer (a pred in another cl). To reach the counters |
| 224 | +or the owning cl they walk up via `slot->slab->owner`. The slab knows its slots |
| 225 | +(inline array); the slot must know its slab for the reverse direction. |
| 226 | + |
| 227 | +## Counters |
| 228 | + |
| 229 | +| counter | on | meaning | |
| 230 | +|---|---|---| |
| 231 | +| `n_used` | slab | slots ever handed out (monotonic) | |
| 232 | +| `n_held` | slab | slots not yet `_slot_release`'d (`n_used` − releases) | |
| 233 | +| `n_pinned` | slab | >0 only when **detached**: # slots still referenced cross-cl | |
| 234 | +| `refs` | slot | # live downstream slots whose `preds[]` point at this slot | |
| 235 | + |
| 236 | +`refs` is a **count, not a list** — we only ask "does anyone still depend on |
| 237 | +me?", never "who?". A slot is freed only when `live==0` (drained itself) AND |
| 238 | +`refs==0` (nobody points at it); the last dependent to drop the ref frees it. |
| 239 | +That is what lets us avoid a reverse index (see the detach note below). |
| 240 | + |
| 241 | +## Slot state machine |
| 242 | + |
| 243 | +```mermaid |
| 244 | +stateDiagram-v2 |
| 245 | + [*] --> LIVE: _slot_instantiate<br/>(resolve preds, preds[i]->refs++) |
| 246 | + LIVE --> DRAINED: _slot_drain<br/>(recurse preds, emit tracepoint,<br/>--pred->refs each) |
| 247 | + DRAINED --> RELEASED: _slot_release<br/>(iff refs==0: dispose inj, --slab.n_held) |
| 248 | + RELEASED --> [*]: slab freed when<br/>n_held==0 && not tail |
| 249 | +``` |
| 250 | + |
| 251 | +## Teardown paths |
| 252 | + |
| 253 | +```mermaid |
| 254 | +graph TD |
| 255 | + A["imm slot drains, unreferenced"] --> A1["_slot_drain -> _slot_release"] |
| 256 | + B["cl RESET / single DESTROY (ctx alive)"] --> B1["_cl_reclaim_slabs, per slab"] |
| 257 | + B1 --> B2{"any slot still ref'd cross-cl?"} |
| 258 | + B2 -- no --> B3["free slab now"] |
| 259 | + B2 -- yes --> B4["DETACH: owner=NULL, n_pinned;<br/>referrer's later drain frees it"] |
| 260 | + C["ctx DESTROY"] --> C1["_cl_data_destroy(ctx_destroy=1):<br/>destroy L0 event + free slot struct"] |
| 261 | + D["event DESTROY"] --> D1["_on_destroy_event: evict<br/>_ze_event_state entry (kts/latest)"] |
| 262 | +``` |
| 263 | + |
| 264 | +**Detach (`owner=NULL`).** Consider: |
| 265 | + |
| 266 | +``` |
| 267 | +CL1.Append(signal=e1) // slot P |
| 268 | +CL2.Append(signal=e2, wait=e1) // slot Q; Q.preds=[P], so P.refs=1 |
| 269 | +CL1.destroy() // P still referenced by Q -> P DETACHED |
| 270 | +CL2.sync() // drains Q, --P.refs -> 0, frees P's slab |
| 271 | +``` |
| 272 | + |
| 273 | +At `CL1.destroy()`, `CL2` still has a pred pointing into `CL1`'s slab (legal L0 — |
| 274 | +there is no ordering rule for cross-cl event deps, unlike fences/queues), so |
| 275 | +freeing that slab would dangle `Q.preds[]`. Instead the slab is *detached* |
| 276 | +(unlinked, `owner=NULL`, `n_pinned` = referenced slots) and outlives `CL1`. Then |
| 277 | +`CL2.sync()` drains `Q`, drops the last ref on `P` (`--P.refs -> 0`), and *that* |
| 278 | +downstream drain frees the detached slab (`n_pinned -> 0`). |
| 279 | + |
| 280 | +(If `CL1` were synced+destroyed *before* `CL2`'s Append, `e1` would already have |
| 281 | +drained, the `wait=e1` would resolve to nothing, and no cross-cl edge — hence no |
| 282 | +detach — would exist. Detach only matters while the dependency is still live.) |
| 283 | + |
| 284 | +The alternative — severing `CL2`'s edges at `CL1` teardown — is rejected: edges |
| 285 | +are one-directional (`preds[]` + a `refs` count, no reverse index), so finding |
| 286 | +referrers would need an O(all-live-slots) global scan or a per-Append successor |
| 287 | +list. Detach is what keeps `refs` a mere count. |
0 commit comments