mirror of
https://github.com/Niko1221/Strata.git
synced 2026-11-10 03:41:36 -10:00
tools/make_profile.py builds the ranking as the base's order, then the trace's pairs, then the fill. The base is only appended to, so --base can never move a pair the base already ranks - and the shipped data/expert-profile.bin ranks all 24,576 pairs, which makes a trace a no-op: wrote ...: 24576 ranked pairs (24576 from the base, 0 from the traces, ...) So the documented way to seed a profile from --dump-routing (rank the pairs your workload routes most) silently did nothing with the shipped base; only --no-base changed anything, and that drops the base's ranking entirely. Add --reorder: rank the trace's pairs first (most frequent first), then let the base fill the rest, then the fill. The default is unchanged. The ranking moves into a rank_profile() so it can be tested without a model; tools/test_make_profile.py covers the no-op, --reorder, --no-base, the frequency/tie order and an end-to-end run over a complete base. On this host's coding trace the same command now puts 5,808 trace pairs first.
120 lines
5.7 KiB
Python
120 lines
5.7 KiB
Python
"""tools/make_profile.py - the expert-cache profile (`data/expert-profile.bin`): every (layer, expert) pair, ranked.
|
|
|
|
`--expert-cache auto` fills the VRAM slots it can afford with the profile's pairs in order, so the ranking decides
|
|
which experts start resident (the adaptive tier then swaps in what a conversation routes most). A profile that
|
|
ranks fewer pairs than a card can hold caps the cache (issue #46: a 32 GB card stopped at 8,000 slots); this tool
|
|
writes one that ranks all 24,576.
|
|
|
|
The order: the base profile's ranking (default: the shipped data/expert-profile.bin), then the pairs your routing
|
|
traces used, most frequent first, then every pair still missing, interleaved across the layers. The base is only
|
|
*appended to*, never reordered - and the shipped profile already ranks all 24,576 pairs, so with it a trace is a
|
|
no-op. `--reorder` ranks the traces first and lets the base fill the rest, which is how a workload's trace decides
|
|
the top; `--no-base` drops the base entirely.
|
|
|
|
python tools/make_profile.py [TRACE ...] [--base data/expert-profile.bin | --no-base] [--reorder] [--out PATH]
|
|
[--n-expert 256] (a pruned model: GSQ-RCO Coder keeps 256 of 512)
|
|
|
|
A routing trace comes from a one-shot engine run with `--dump-routing FILE` (a prompt typical of your use; the
|
|
routed experts of every layer and position are written). Point the model config's `--expert-profile` at the result.
|
|
"""
|
|
import argparse
|
|
import struct
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
N_LAYER, N_EXPERT = 48, 512
|
|
MAGIC, VERSION = b"STRP", 1
|
|
|
|
|
|
def read_profile(path, n_expert=N_EXPERT):
|
|
blob = Path(path).read_bytes()
|
|
if blob[:4] != MAGIC:
|
|
raise SystemExit(f"{path}: not a Strata profile")
|
|
ver, nl, ne, slots, n = struct.unpack_from("<5I", blob, 4)
|
|
if (nl, ne) != (N_LAYER, n_expert):
|
|
raise SystemExit(f"{path}: {nl}x{ne}, not {N_LAYER}x{n_expert}")
|
|
return [struct.unpack_from("<HH", blob, 24 + 4 * i) for i in range(n)]
|
|
|
|
|
|
def read_trace(path, n_expert=N_EXPERT):
|
|
"""(layer, k, k expert ids, k weights) records, as `--dump-routing` writes them."""
|
|
blob = Path(path).read_bytes()
|
|
off, freq = 0, defaultdict(int)
|
|
while off + 8 <= len(blob):
|
|
layer, k = struct.unpack_from("<ii", blob, off)
|
|
off += 8
|
|
for e in struct.unpack_from("<%di" % k, blob, off):
|
|
if 0 <= layer < N_LAYER and 0 <= e < n_expert:
|
|
freq[(layer, e)] += 1
|
|
off += 8 * k # the ids and the weights
|
|
return freq
|
|
|
|
|
|
def write_profile(path, ranked, n_expert=N_EXPERT):
|
|
table = [[-1] * n_expert for _ in range(N_LAYER)]
|
|
for slot, (layer, e) in enumerate(ranked):
|
|
table[layer][e] = slot
|
|
with open(path, "wb") as f:
|
|
f.write(MAGIC + struct.pack("<5I", VERSION, N_LAYER, n_expert, len(ranked), len(ranked)))
|
|
for layer, e in ranked:
|
|
f.write(struct.pack("<HH", layer, e))
|
|
for layer in range(N_LAYER):
|
|
f.write(struct.pack("<%di" % n_expert, *table[layer]))
|
|
|
|
|
|
def rank_profile(base_pairs, trace_freq, n_expert=N_EXPERT, no_base=False, reorder=False):
|
|
"""The ranked (layer, expert) list and the counts it was built from (`base`, `trace`, `fill`).
|
|
|
|
Default: the base's order, then the traces' pairs most frequent first, then the fill. With `reorder` (or
|
|
`no_base`) the traces' pairs come first, so a base that already ranks every pair no longer hides them; the base
|
|
(unless `no_base`) and then the fill follow. A base is never reordered against itself - it only ever fills."""
|
|
ranked, seen, counts = [], set(), defaultdict(int)
|
|
|
|
def take(pairs, key):
|
|
for p in pairs:
|
|
p = (int(p[0]), int(p[1]))
|
|
if p not in seen:
|
|
seen.add(p)
|
|
ranked.append(p)
|
|
counts[key] += 1
|
|
|
|
trace_pairs = [p for p, _ in sorted(trace_freq.items(), key=lambda kv: (-kv[1], kv[0]))]
|
|
if no_base or reorder:
|
|
take(trace_pairs, "trace")
|
|
if not no_base:
|
|
take(base_pairs, "base")
|
|
if not (no_base or reorder):
|
|
take(trace_pairs, "trace")
|
|
take(((layer, e) for e in range(n_expert) for layer in range(N_LAYER)), "fill")
|
|
return ranked, counts
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
|
|
ap.add_argument("traces", nargs="*", help="routing traces from --dump-routing")
|
|
ap.add_argument("--base", default=str(ROOT / "data" / "expert-profile.bin"), help="ranking to keep first")
|
|
ap.add_argument("--no-base", action="store_true", help="rank by the traces only")
|
|
ap.add_argument("--reorder", action="store_true",
|
|
help="rank the traces' pairs before the base's (the base, then the fill, follow); needed with a "
|
|
"base that already ranks every pair, where --base alone cannot change the order")
|
|
ap.add_argument("--out", default=str(ROOT / "data" / "expert-profile.bin"))
|
|
ap.add_argument("--n-expert", type=int, default=N_EXPERT, help="experts per layer (default 512)")
|
|
a = ap.parse_args()
|
|
|
|
ne = a.n_expert
|
|
freq = defaultdict(int)
|
|
for t in a.traces:
|
|
for p, c in read_trace(t, ne).items():
|
|
freq[p] += c
|
|
base_pairs = [] if a.no_base else read_profile(a.base, ne)
|
|
ranked, counts = rank_profile(base_pairs, freq, ne, no_base=a.no_base, reorder=a.reorder)
|
|
write_profile(a.out, ranked, ne)
|
|
assert read_profile(a.out, ne) == ranked, "the profile did not survive the round trip"
|
|
print(f"wrote {a.out}: {len(ranked)} ranked pairs ({counts['base']} from the base, {counts['trace']} from the "
|
|
f"traces, {counts['fill']} filled in)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|