File size: 11,196 Bytes
0693474
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
"""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]