dsv41-offload / offload /kl1 /trace_kernels.py
ProprietaryLegal's picture
PLI Labs public snapshot 2026-10-28 (history squashed)
0693474
Raw History Blame Contribute Delete
11.2 kB
"""Kernel-level view of the SP1 kineto traces: per-step kernel families, per-layer MoE spans and collective pairs.
Read only. Step periods follow offload/gb10_budget/trace_budget.py exactly (an iteration's GPU period runs from its first
own activity to the next iteration's first own activity; the last iteration has no successor and is dropped), so
per-step numbers here line up with SP1's trace-budget.json. All durations are kineto's (MEASURED, profile-affected:
SP1 measured decode overhead 1.8-4.5 %, prefill 24.9 %).
"""
from __future__ import annotations
import bisect
import gzip
import re
from collections.abc import Iterable, Sequence
from dataclasses import dataclass, field
from pathlib import Path
from typing import IO, cast
from offload.gb10_budget.trace_budget import (CALL_KINDS, GPU_KINDS, MAX_TRACE_BYTES, Rules, bucket, details, events,
rules)
from offload.prefill_maxlen.evidence import Json, number, record
# Ordered: first match wins. Finer than buckets.json; the SP1 bucket of every kernel is kept beside its family.
FAMILIES: tuple[tuple[str, re.Pattern[str]], ...] = tuple((f, re.compile(p)) for f, p in (
('l2pf', r'\bl2pf_kernel\b'),
('smoe_rot', r'\bsmoe_rot\b'),
('smoe_gu', r'\bsmoe_gu\b'),
('smoe_down', r'\bsmoe_down\b'),
('smoe_fused', r'\bsmoe_fused\b'),
('router', r'\brouter_kernel\b'),
('route_topk', r'\b_dsv4_topk_kernel\b'),
('tf_gu', r'\bgrouped_mma\w*kernel\b'),
('tf_down', r'\bgrouped_rows\w*kernel\b'),
('tf_aux', r'\brot_in_kernel\b|\bgateup_epilogue_kernel\b|\bdown_combine_kernel\b|\bdecode_prep_kernel\b|'
r'\bwork_list_kernel\b|\bgroup_(count|place)?_?kernel\b|\bcombine(_y)?_kernel\b'),
('mrow_head', r'\bmrow(32)?_kernel<6\b'),
('mrow', r'\bmrow(32)?_kernel<'),
('exl3_gemm', r'\bexl3_gemm_kernel<'),
('reconstruct', r'\breconstruct_had_kernel<'),
('marlin', r'[Mm]arlin'),
('hc_prenorm', r'sm120_tf32_hc_prenorm_gemm'),
('mqa_logits', r'paged_mqa_logits'),
('gemm_other', r'cutlass|nvjet|cublas|gemm|Gemm|GEMM|xmma'),
('coll_stage', r'\bstage_kernel<'),
('coll_sum', r'\bsum_kernel\('),
('coll_gather', r'::gather_kernel<'),
('nccl', r'nccl'),
('sparse_mla', r'sparse_mla'),
))
COLLECTIVE = ('coll_stage', 'coll_sum', 'coll_gather', 'nccl')
TEMPLATE_K = re.compile(r'(?:mrow(?:32)?_kernel|exl3_gemm_kernel|reconstruct_had_kernel)<(\d+)')
def family(name: str) -> str:
return next((f for f, p in FAMILIES if p.search(name)), 'rest')
def template_k(name: str) -> int | None:
m = TEMPLATE_K.search(name)
return int(m.group(1)) if m else None
@dataclass(frozen=True)
class Kernel:
name: str
family: str
bucket: str # SP1's buckets.json bucket (l2pf is 'routed_moe' there)
start: float # us, kineto timebase of this rank
end: float
stream: int
grid: tuple[int, int, int]
block: tuple[int, int, int]
blocks_per_sm: float | None # kineto's theoretical active blocks per SM (occupancy record)
corr: int | None
@property
def dur(self) -> float:
return self.end - self.start
@dataclass(frozen=True)
class Call:
name: str
start: float
end: float
tid: str
corr: int | None
@dataclass(frozen=True)
class Annotation:
name: str
start: float
end: float
tid: str
@dataclass
class RankTrace:
kernels: list[Kernel] # GPU kernels, memcpys and memsets, by start
calls: list[Call]
annotations: list[Annotation]
@dataclass
class Step:
index: int
annotation: str
phase: str
rows: int # scheduled tokens of the phase (generation tokens for decode)
requests: int
lo: float
hi: float
kernels: list[Kernel] = field(default_factory=list)
@property
def period(self) -> float:
return self.hi - self.lo
def _triple(value: Json) -> tuple[int, int, int]:
if not isinstance(value, list) or len(value) != 3:
return (0, 0, 0)
a, b, c = (int(number(v)) for v in value)
return (a, b, c)
def parse(handle: IO[str], r: Rules) -> RankTrace:
kernels: list[Kernel] = []
calls: list[Call] = []
annotations: list[Annotation] = []
for event in events(handle):
cat = event.get('cat')
if event.get('ph') != 'X' or not isinstance(cat, str):
continue
name = str(event.get('name', ''))
start = number(event['ts'])
end = start + number(event.get('dur', 0))
args = record(event.get('args') or {})
corr = None if args.get('correlation') is None else int(number(args['correlation']))
if cat in GPU_KINDS:
occ = args.get('occupancy')
bps = None
if isinstance(occ, dict) and occ.get('activeBlocksPerMultiprocessor') is not None:
bps = number(occ['activeBlocksPerMultiprocessor'])
stream = args.get('stream')
kernels.append(Kernel(name, family(name), bucket(name, r), start, end,
-1 if stream is None else int(number(stream)), _triple(args.get('grid')),
_triple(args.get('block')), bps, corr))
elif cat in CALL_KINDS:
calls.append(Call(name, start, end, str(event.get('tid')), corr))
elif cat == 'user_annotation' and name.startswith('execute_'):
annotations.append(Annotation(name, start, end, str(event.get('tid'))))
kernels.sort(key=lambda k: (k.start, k.end))
calls.sort(key=lambda c: c.start)
annotations.sort(key=lambda a: a.start)
return RankTrace(kernels, calls, annotations)
def load(path: Path, r: Rules | None = None) -> RankTrace:
if path.stat().st_size > MAX_TRACE_BYTES:
raise ValueError(f'{path.name}: larger than the per-file cap')
with gzip.open(path, 'rt', encoding='utf-8') as handle:
trace = parse(cast(IO[str], handle), r or rules())
while handle.read(1 << 20):
pass
return trace
def steps(trace: RankTrace) -> list[Step]:
"""Complete iterations with their GPU period [first own activity, next iteration's first own activity)."""
by_corr: dict[int, float] = {}
for k in trace.kernels:
if k.corr is not None:
by_corr[k.corr] = min(by_corr.get(k.corr, k.start), k.start)
call_starts = [c.start for c in trace.calls]
firsts: list[float | None] = []
for ann in trace.annotations:
lo: float | None = None
for c in trace.calls[bisect.bisect_left(call_starts, ann.start):bisect.bisect_right(call_starts, ann.end)]:
if c.tid == ann.tid and c.corr is not None and c.corr in by_corr:
t = by_corr[c.corr]
lo = t if lo is None else min(lo, t)
firsts.append(lo)
starts = [k.start for k in trace.kernels]
out: list[Step] = []
for i, ann in enumerate(trace.annotations[:-1]):
lo, hi = firsts[i], firsts[i + 1]
if lo is None or hi is None or hi <= lo:
continue
d = details(ann.name)
phase = str(d['phase'])
rows = d['generation_tokens'] if phase == 'decode' else d['context_tokens']
reqs = d['generation_requests'] if phase == 'decode' else d['context_requests']
mine = trace.kernels[bisect.bisect_left(starts, lo):bisect.bisect_left(starts, hi)]
out.append(Step(i, ann.name, phase, int(number(rows or 0)), int(number(reqs or 0)), lo, hi, list(mine)))
return out
def union(spans: Iterable[tuple[float, float]], lo: float = float('-inf'), hi: float = float('inf')) -> float:
total = 0.0
cur_s = cur_e = None
for s, e in sorted((max(s, lo), min(e, hi)) for s, e in spans):
if e <= s:
continue
if cur_e is None or s > cur_e:
if cur_e is not None and cur_s is not None:
total += cur_e - cur_s
cur_s, cur_e = s, e
else:
cur_e = max(cur_e, e)
if cur_e is not None and cur_s is not None:
total += cur_e - cur_s
return total
@dataclass(frozen=True)
class MoeLayer:
"""One layer's routed-expert launches in a step (decode: smoe rot/gu/down; prefill: TF grouped gate|up / down)."""
router: Kernel | None
topk: Kernel | None
rot: Kernel | None
gu: Kernel
down: Kernel
@property
def start(self) -> float:
return self.rot.start if self.rot is not None else self.gu.start
@property
def span(self) -> float:
return max(self.down.end, self.gu.end) - self.start
@property
def down_after_gu(self) -> float:
return max(0.0, self.down.end - self.gu.end)
def moe_layers(step: Step) -> list[MoeLayer]:
ks = step.kernels
out: list[MoeLayer] = []
router: Kernel | None = None
topk: Kernel | None = None
rot: Kernel | None = None
gu: Kernel | None = None
for k in ks:
if k.family == 'router':
router = k
elif k.family == 'route_topk':
topk = k
elif k.family in ('smoe_rot',):
rot = k
elif k.family in ('smoe_gu', 'tf_gu'):
gu = k
elif k.family in ('smoe_down', 'tf_down') and gu is not None:
out.append(MoeLayer(router, topk, rot, gu, k))
router = topk = rot = gu = None
return out
def family_totals(step: Step) -> dict[str, dict[str, float]]:
"""Per family: launches, summed kernel time and union (wall) time inside the step period."""
groups: dict[str, list[Kernel]] = {}
for k in step.kernels:
groups.setdefault(k.family, []).append(k)
return {f: {'n': float(len(v)), 'sum_us': sum(min(k.end, step.hi) - k.start for k in v),
'union_us': union(((k.start, k.end) for k in v), step.lo, step.hi)}
for f, v in sorted(groups.items())}
def collective_calls(step: Step) -> list[Kernel]:
return [k for k in step.kernels if k.family in COLLECTIVE]
def compute_segments(step: Step) -> list[tuple[str, float]]:
"""Main-stream work between collectives: (kind, us) from the end of one all-reduce's sum to the next stage. Kind is
'moe' when the segment holds smoe/TF expert kernels, 'attn' otherwise (the attention half of a layer)."""
marks = [k for k in step.kernels if k.family in ('coll_stage', 'coll_sum')]
segs: list[tuple[str, float]] = []
prev_end: float | None = None
for k in marks:
if k.family == 'coll_stage' and prev_end is not None:
inside = [x for x in step.kernels if prev_end <= x.start < k.start]
kind = 'moe' if any(x.family in ('smoe_gu', 'tf_gu') for x in inside) else 'attn'
segs.append((kind, k.start - prev_end))
prev_end = None
elif k.family == 'coll_sum':
prev_end = k.end
return segs
def pair_steps(a: Sequence[Step], b: Sequence[Step]) -> list[tuple[Step, Step]]:
"""Steps of the two ranks with the same iteration index and annotation (the ranks run the same schedule)."""
bi = {s.index: s for s in b}
return [(s, bi[s.index]) for s in a if s.index in bi and bi[s.index].annotation == s.annotation]