forked from zhoulin/zl_bench
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
abf9475ff3 | ||
|
|
7522143747 | ||
|
|
3c9529dab2 | ||
|
|
64baa11a23 |
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
# Everything here runs on Linux accelerator hosts, and campaign receipts record
|
||||||
|
# the SHA-256 of the harness files they were produced by. A CRLF checkout on
|
||||||
|
# Windows would both break the shell drivers and change every recorded digest,
|
||||||
|
# so pin the text formats this repository ships.
|
||||||
|
*.py text eol=lf
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.json text eol=lf
|
||||||
|
*.yaml text eol=lf
|
||||||
|
*.csv text eol=lf
|
||||||
|
*.tsv text eol=lf
|
||||||
|
*.txt text eol=lf
|
||||||
|
*.md text eol=lf
|
||||||
+142
-31
@@ -1,4 +1,4 @@
|
|||||||
"""pytest plugin: swap `triton.testing.do_bench` for cudagraph-based timing.
|
"""pytest plugin: swap `triton.testing.do_bench` for graph-capture timing.
|
||||||
|
|
||||||
Loaded via `-p _cudagraph_plugin` in run_pytest.sh's PLUGINS list (comment that
|
Loaded via `-p _cudagraph_plugin` in run_pytest.sh's PLUGINS list (comment that
|
||||||
line out to time with plain do_bench). Monkey-patches do_bench →
|
line out to time with plain do_bench). Monkey-patches do_bench →
|
||||||
@@ -8,9 +8,18 @@ fall back to plain do_bench and print a BENCHMARK_DIRECT_NO_CUDAGRAPH marker
|
|||||||
into the log. Before capturing, fn is explicitly warmed up for the caller's
|
into the log. Before capturing, fn is explicitly warmed up for the caller's
|
||||||
warmup time budget (do_bench_cudagraph's own 5-iter warmup is too short to
|
warmup time budget (do_bench_cudagraph's own 5-iter warmup is too short to
|
||||||
settle autotune/JIT for the MoE kernels, which makes the first captured
|
settle autotune/JIT for the MoE kernels, which makes the first captured
|
||||||
measurement unstable run-to-run). The capture itself runs under a scratch CUDA
|
measurement unstable run-to-run). The capture itself runs under a scratch RNG
|
||||||
RNG state so that an aborted capture cannot poison the process-wide generator
|
state so that an aborted capture cannot poison the process-wide generator
|
||||||
(rationale: _capture_with_scratch_rng).
|
(rationale: _capture_with_scratch_rng).
|
||||||
|
|
||||||
|
Everything here goes through Triton's active device interface rather than
|
||||||
|
torch.cuda. FlagTree's do_bench is already written that way, but its
|
||||||
|
do_bench_cudagraph is not: it hardcodes torch.cuda.stream/Event/CUDAGraph, so
|
||||||
|
on a non-CUDA backend the graph path raised and every measurement silently fell
|
||||||
|
back to plain do_bench. On MThreads the interface is torch_musa, which supplies
|
||||||
|
Stream, Event, synchronize, graph and MUSAGraph, so the same capture works once
|
||||||
|
the calls are taken from the interface. CUDA still calls upstream's
|
||||||
|
do_bench_cudagraph, so its numbers are unchanged.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -24,8 +33,33 @@ import triton.testing as _tt
|
|||||||
_ORIGINAL_DO_BENCH = _tt.do_bench
|
_ORIGINAL_DO_BENCH = _tt.do_bench
|
||||||
|
|
||||||
|
|
||||||
def _is_fatal_cuda_error(exc: Exception) -> bool:
|
def _device_interface():
|
||||||
"""True for CUDA context errors (illegal address, OOM, device-side assert):
|
"""Triton's active device interface: torch.cuda on NVIDIA, torch_musa on MThreads."""
|
||||||
|
from triton.runtime import driver
|
||||||
|
|
||||||
|
return driver.active.get_device_interface()
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cuda(di) -> bool:
|
||||||
|
import torch
|
||||||
|
|
||||||
|
return di is torch.cuda
|
||||||
|
|
||||||
|
|
||||||
|
def _graph_class(di):
|
||||||
|
"""The interface's graph type: CUDAGraph, MUSAGraph, or whatever it names."""
|
||||||
|
for name in ("CUDAGraph", "MUSAGraph"):
|
||||||
|
graph = getattr(di, name, None)
|
||||||
|
if graph is not None:
|
||||||
|
return graph
|
||||||
|
for name in dir(di):
|
||||||
|
if name.endswith("Graph"):
|
||||||
|
return getattr(di, name)
|
||||||
|
raise RuntimeError("device interface exposes no graph type")
|
||||||
|
|
||||||
|
|
||||||
|
def _is_fatal_device_error(exc: Exception) -> bool:
|
||||||
|
"""True for device context errors (illegal address, OOM, device-side assert):
|
||||||
the context is poisoned, so re-raise rather than fall back and record a bogus
|
the context is poisoned, so re-raise rather than fall back and record a bogus
|
||||||
latency as success.
|
latency as success.
|
||||||
|
|
||||||
@@ -47,8 +81,12 @@ def _is_fatal_cuda_error(exc: Exception) -> bool:
|
|||||||
return any(m in msg for m in markers)
|
return any(m in msg for m in markers)
|
||||||
|
|
||||||
|
|
||||||
def _warmup_before_capture(fn, warmup_ms, grad_to_none):
|
# Retained under its historical name; the marker list is backend independent.
|
||||||
"""Warm fn up before cudagraph capture.
|
_is_fatal_cuda_error = _is_fatal_device_error
|
||||||
|
|
||||||
|
|
||||||
|
def _warmup_before_capture(di, fn, warmup_ms, grad_to_none):
|
||||||
|
"""Warm fn up before graph capture.
|
||||||
|
|
||||||
do_bench_cudagraph only warms up 5 iterations internally — too few for the
|
do_bench_cudagraph only warms up 5 iterations internally — too few for the
|
||||||
MoE kernels, where the first calls trigger Triton autotune config
|
MoE kernels, where the first calls trigger Triton autotune config
|
||||||
@@ -66,8 +104,6 @@ def _warmup_before_capture(fn, warmup_ms, grad_to_none):
|
|||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import torch
|
|
||||||
|
|
||||||
def _run():
|
def _run():
|
||||||
if grad_to_none is not None:
|
if grad_to_none is not None:
|
||||||
for x in grad_to_none:
|
for x in grad_to_none:
|
||||||
@@ -76,23 +112,23 @@ def _warmup_before_capture(fn, warmup_ms, grad_to_none):
|
|||||||
|
|
||||||
# First call absorbs autotune/JIT compilation (can take seconds); discard it.
|
# First call absorbs autotune/JIT compilation (can take seconds); discard it.
|
||||||
_run()
|
_run()
|
||||||
torch.cuda.synchronize()
|
di.synchronize()
|
||||||
# Size the warmup loop from one timed steady-state run. dt_ms includes
|
# Size the warmup loop from one timed steady-state run. dt_ms includes
|
||||||
# launch overhead, so n errs low for tiny kernels; warmup needn't be exact.
|
# launch overhead, so n errs low for tiny kernels; warmup needn't be exact.
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
_run()
|
_run()
|
||||||
torch.cuda.synchronize()
|
di.synchronize()
|
||||||
dt_ms = (time.perf_counter() - t0) * 1000.0
|
dt_ms = (time.perf_counter() - t0) * 1000.0
|
||||||
budget = warmup_ms if (warmup_ms and warmup_ms > 0) else 25.0
|
budget = warmup_ms if (warmup_ms and warmup_ms > 0) else 25.0
|
||||||
n = int(budget / dt_ms) if dt_ms > 0 else 25
|
n = int(budget / dt_ms) if dt_ms > 0 else 25
|
||||||
n = max(5, min(n, 200))
|
n = max(5, min(n, 200))
|
||||||
for _ in range(n):
|
for _ in range(n):
|
||||||
_run()
|
_run()
|
||||||
torch.cuda.synchronize()
|
di.synchronize()
|
||||||
|
|
||||||
|
|
||||||
def _capture_with_scratch_rng(call):
|
def _capture_with_scratch_rng(di, call):
|
||||||
"""Run a graph capture under a throwaway CUDA RNG state.
|
"""Run a graph capture under a throwaway device RNG state.
|
||||||
|
|
||||||
torch.cuda.graph's capture_begin unconditionally registers the default CUDA
|
torch.cuda.graph's capture_begin unconditionally registers the default CUDA
|
||||||
RNG generator and sets its capturing flag; when a capture aborts midway the
|
RNG generator and sets its capturing flag; when a capture aborts midway the
|
||||||
@@ -104,12 +140,21 @@ def _capture_with_scratch_rng(call):
|
|||||||
before capturing and restore the original state object afterwards, success
|
before capturing and restore the original state object afterwards, success
|
||||||
or failure. The real generator never takes part in a capture, so poisoning
|
or failure. The real generator never takes part in a capture, so poisoning
|
||||||
can only land on the discarded scratch state, and _seed_plugin determinism
|
can only land on the discarded scratch state, and _seed_plugin determinism
|
||||||
is left untouched."""
|
is left untouched.
|
||||||
|
|
||||||
|
An interface that does not expose default_generators has no such registration
|
||||||
|
to protect against (torch_musa is one), so the capture simply runs.
|
||||||
|
"""
|
||||||
import torch
|
import torch
|
||||||
dev = torch.cuda.current_device()
|
|
||||||
gen = torch.cuda.default_generators[dev]
|
generators = getattr(di, "default_generators", None)
|
||||||
|
if not generators:
|
||||||
|
return call()
|
||||||
|
gen = generators[di.current_device()]
|
||||||
|
if not hasattr(gen, "graphsafe_get_state"):
|
||||||
|
return call()
|
||||||
saved = gen.graphsafe_get_state()
|
saved = gen.graphsafe_get_state()
|
||||||
scratch = torch.Generator(device="cuda")
|
scratch = torch.Generator(device=torch.device(gen.device))
|
||||||
scratch.manual_seed(gen.initial_seed())
|
scratch.manual_seed(gen.initial_seed())
|
||||||
gen.graphsafe_set_state(scratch)
|
gen.graphsafe_set_state(scratch)
|
||||||
try:
|
try:
|
||||||
@@ -118,24 +163,85 @@ def _capture_with_scratch_rng(call):
|
|||||||
gen.graphsafe_set_state(saved)
|
gen.graphsafe_set_state(saved)
|
||||||
|
|
||||||
|
|
||||||
|
def _do_bench_graph(di, fn, rep, grad_to_none, quantiles, return_mode, n_retries=10):
|
||||||
|
"""do_bench_cudagraph, taken from the device interface instead of torch.cuda.
|
||||||
|
|
||||||
|
A step-for-step port of upstream's implementation: estimate one iteration,
|
||||||
|
unroll that many calls into a captured graph so host launch overhead drops
|
||||||
|
out, then time independent replays. Upstream is used verbatim on CUDA; this
|
||||||
|
exists because upstream names torch.cuda directly and so cannot run on any
|
||||||
|
other backend.
|
||||||
|
|
||||||
|
The replay timings are handed to _summarize_statistics as a plain list of
|
||||||
|
floats, which is what both do_bench and upstream's do_bench_cudagraph pass:
|
||||||
|
this fork's helper runs Python's statistics module over them, so wrapping
|
||||||
|
them in a tensor makes "mean" raise and makes a quantile request return
|
||||||
|
tensors that later fail to serialize.
|
||||||
|
"""
|
||||||
|
with di.stream(di.Stream()):
|
||||||
|
fn()
|
||||||
|
if grad_to_none is not None:
|
||||||
|
for x in grad_to_none:
|
||||||
|
x.detach_()
|
||||||
|
x.requires_grad_(True)
|
||||||
|
x.grad = None
|
||||||
|
start_event = di.Event(enable_timing=True)
|
||||||
|
end_event = di.Event(enable_timing=True)
|
||||||
|
start_event.record()
|
||||||
|
for _ in range(5):
|
||||||
|
fn()
|
||||||
|
end_event.record()
|
||||||
|
di.synchronize()
|
||||||
|
estimate_ms = start_event.elapsed_time(end_event) / 5
|
||||||
|
n_repeat = 1000 if estimate_ms == 0 else max(1, int(rep / estimate_ms))
|
||||||
|
graph = _graph_class(di)()
|
||||||
|
with di.graph(graph):
|
||||||
|
for _ in range(n_repeat):
|
||||||
|
if grad_to_none is not None:
|
||||||
|
for x in grad_to_none:
|
||||||
|
x.grad = None
|
||||||
|
fn()
|
||||||
|
di.synchronize()
|
||||||
|
ret = []
|
||||||
|
for _ in range(n_retries):
|
||||||
|
start_event = di.Event(enable_timing=True)
|
||||||
|
end_event = di.Event(enable_timing=True)
|
||||||
|
start_event.record()
|
||||||
|
graph.replay()
|
||||||
|
end_event.record()
|
||||||
|
di.synchronize()
|
||||||
|
ret.append(start_event.elapsed_time(end_event) / n_repeat)
|
||||||
|
del graph
|
||||||
|
return _tt._summarize_statistics(ret, quantiles, return_mode)
|
||||||
|
|
||||||
|
|
||||||
def _patched_do_bench(fn, warmup=25, rep=100, grad_to_none=None,
|
def _patched_do_bench(fn, warmup=25, rep=100, grad_to_none=None,
|
||||||
quantiles=None, return_mode="mean"):
|
quantiles=None, return_mode="mean"):
|
||||||
"""Drop-in replacement for triton.testing.do_bench using cudagraph capture."""
|
"""Drop-in replacement for triton.testing.do_bench using graph capture."""
|
||||||
op = os.environ.get("FLAGGEMS_PERF_CURRENT_OP", "<unknown>")
|
op = os.environ.get("FLAGGEMS_PERF_CURRENT_OP", "<unknown>")
|
||||||
phase = "sync" # which step rejected cudagraph, for the fallback marker
|
phase = "sync" # which step rejected graph capture, for the fallback marker
|
||||||
try:
|
try:
|
||||||
# do_bench_cudagraph runs fn on a fresh side stream without syncing
|
di = _device_interface()
|
||||||
# with the default stream first; input tensors produced just before
|
# The graph path runs fn on a fresh side stream without syncing with the
|
||||||
# (quantize/topk in the input iter) may still be in flight, and racing
|
# default stream first; input tensors produced just before (quantize/topk
|
||||||
# on them intermittently kills the context (illegal instruction from a
|
# in the input iter) may still be in flight, and racing on them
|
||||||
# device trap on garbage indices). Sync before switching streams.
|
# intermittently kills the context (illegal instruction from a device
|
||||||
import torch
|
# trap on garbage indices). Sync before switching streams.
|
||||||
torch.cuda.synchronize()
|
di.synchronize()
|
||||||
phase = "warmup"
|
phase = "warmup"
|
||||||
# Settle autotune/JIT before capture (rationale: _warmup_before_capture).
|
# Settle autotune/JIT before capture (rationale: _warmup_before_capture).
|
||||||
_warmup_before_capture(fn, warmup, grad_to_none)
|
_warmup_before_capture(di, fn, warmup, grad_to_none)
|
||||||
phase = "capture"
|
phase = "capture"
|
||||||
return _capture_with_scratch_rng(lambda: _tt.do_bench_cudagraph(
|
if _is_cuda(di):
|
||||||
|
return _capture_with_scratch_rng(di, lambda: _tt.do_bench_cudagraph(
|
||||||
|
fn,
|
||||||
|
rep=rep,
|
||||||
|
grad_to_none=grad_to_none,
|
||||||
|
quantiles=quantiles,
|
||||||
|
return_mode=return_mode,
|
||||||
|
))
|
||||||
|
return _capture_with_scratch_rng(di, lambda: _do_bench_graph(
|
||||||
|
di,
|
||||||
fn,
|
fn,
|
||||||
rep=rep,
|
rep=rep,
|
||||||
grad_to_none=grad_to_none,
|
grad_to_none=grad_to_none,
|
||||||
@@ -145,7 +251,7 @@ def _patched_do_bench(fn, warmup=25, rep=100, grad_to_none=None,
|
|||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
# Re-raise genuine device failures; anything else falls back to plain
|
# Re-raise genuine device failures; anything else falls back to plain
|
||||||
# do_bench, with the failing phase (sync/warmup/capture) in the marker.
|
# do_bench, with the failing phase (sync/warmup/capture) in the marker.
|
||||||
if _is_fatal_cuda_error(exc):
|
if _is_fatal_device_error(exc):
|
||||||
raise
|
raise
|
||||||
reason = type(exc).__name__
|
reason = type(exc).__name__
|
||||||
detail = " ".join(str(exc).split())[:160]
|
detail = " ".join(str(exc).split())[:160]
|
||||||
@@ -168,5 +274,10 @@ def _patched_do_bench(fn, warmup=25, rep=100, grad_to_none=None,
|
|||||||
def pytest_configure(config):
|
def pytest_configure(config):
|
||||||
from _term_style import tag
|
from _term_style import tag
|
||||||
_tt.do_bench = _patched_do_bench
|
_tt.do_bench = _patched_do_bench
|
||||||
print(f"{tag('[cudagraph-plugin]')} triton.testing.do_bench → do_bench_cudagraph",
|
try:
|
||||||
|
backend = getattr(_device_interface(), "__name__", "?")
|
||||||
|
except Exception: # pragma: no cover - reported, never fatal at configure
|
||||||
|
backend = "?"
|
||||||
|
print(f"{tag('[cudagraph-plugin]')} triton.testing.do_bench → graph capture "
|
||||||
|
f"via {backend}",
|
||||||
file=sys.stderr, flush=True)
|
file=sys.stderr, flush=True)
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
"""Narrow compatibility fixes for the frozen MUSA product corpus.
|
||||||
|
|
||||||
|
Several upstream FlagGems tests gate themselves on ``torch.cuda.is_available()``
|
||||||
|
even though the code under test runs on any accelerator FlagGems dispatches to.
|
||||||
|
On MThreads those guards skip the test instead of exercising the operator, which
|
||||||
|
turns a working path into a verdict of "not measured".
|
||||||
|
|
||||||
|
Every override here is pinned to one file and one exact skip reason, applies
|
||||||
|
only when FlagGems has dispatched to the MThreads vendor, and is only added
|
||||||
|
after the unskipped test was observed to pass on the device.
|
||||||
|
|
||||||
|
Two files were probed on S5000 and deliberately left out, because their guards
|
||||||
|
are not stale:
|
||||||
|
|
||||||
|
- ``test_cutlass_scaled_mm.py`` gates on vLLM as well as CUDA, so removing the
|
||||||
|
guard would raise on the missing dependency instead of measuring anything.
|
||||||
|
- ``test_grid_sample.py`` builds its inputs through a helper whose device
|
||||||
|
argument defaults to the literal ``"cuda"``. Its guard is consistent with its
|
||||||
|
own body: unskipped, all 90 collected cases fail in ``aten::empty`` on a CUDA
|
||||||
|
backend that does not exist here. Making that file portable means editing
|
||||||
|
upstream test source, which is outside what a compatibility plugin may do.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
# file name -> exact skipif reasons that are stale on a dispatching backend.
|
||||||
|
_STALE_CUDA_GUARDS: dict[str, frozenset[str]] = {
|
||||||
|
"test_fft.py": frozenset({"CUDA is not available"}),
|
||||||
|
"test_top_k_per_row_decode.py": frozenset({"CUDA device required"}),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Guards that are only stale for one test in the file stay pinned to it.
|
||||||
|
_PINNED_TESTS: dict[str, str] = {
|
||||||
|
"test_fft.py": "test_perf_fft",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_stale_cuda_guard(item, mark) -> bool:
|
||||||
|
if mark.name != "skipif":
|
||||||
|
return False
|
||||||
|
name = Path(str(item.path)).name
|
||||||
|
reasons = _STALE_CUDA_GUARDS.get(name)
|
||||||
|
if reasons is None or mark.kwargs.get("reason") not in reasons:
|
||||||
|
return False
|
||||||
|
pinned = _PINNED_TESTS.get(name)
|
||||||
|
return pinned is None or item.name == pinned
|
||||||
|
|
||||||
|
|
||||||
|
# Retained under its historical name: the sealed campaign's tests reference it.
|
||||||
|
def _is_stale_fft_cuda_guard(item, mark) -> bool:
|
||||||
|
return Path(str(item.path)).name == "test_fft.py" and _is_stale_cuda_guard(
|
||||||
|
item, mark
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_collection_modifyitems(items):
|
||||||
|
import flag_gems
|
||||||
|
|
||||||
|
if flag_gems.vendor_name != "mthreads":
|
||||||
|
return
|
||||||
|
removed: dict[str, int] = {}
|
||||||
|
# A guard can sit on the test, its class, or the module's pytestmark, and
|
||||||
|
# skipif is resolved by walking that whole chain. Strip every level.
|
||||||
|
seen: set[int] = set()
|
||||||
|
for item in items:
|
||||||
|
for node in item.listchain():
|
||||||
|
if id(node) in seen:
|
||||||
|
continue
|
||||||
|
seen.add(id(node))
|
||||||
|
markers = getattr(node, "own_markers", None)
|
||||||
|
if not markers:
|
||||||
|
continue
|
||||||
|
kept = [mark for mark in markers if not _is_stale_cuda_guard(item, mark)]
|
||||||
|
dropped = len(markers) - len(kept)
|
||||||
|
if dropped:
|
||||||
|
markers[:] = kept
|
||||||
|
name = Path(str(item.path)).name
|
||||||
|
removed[name] = removed.get(name, 0) + dropped
|
||||||
|
for name, count in sorted(removed.items()):
|
||||||
|
print(
|
||||||
|
f"[musa-benchmark-compat] removed {count} stale CUDA-only skip "
|
||||||
|
f"guard(s) from {name}",
|
||||||
|
file=sys.stderr,
|
||||||
|
flush=True,
|
||||||
|
)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# A/B 对比:FlagTree RemoveLayoutConversions 增强关 vs 开,逐算子两轮。
|
||||||
|
#
|
||||||
|
# 口径:每个算子先跑关(record autotune config),再用 REPLAY_FROM 回放同一套
|
||||||
|
# config 跑开,两侧只差 FLAGTREE_<VENDOR>_RLC_ENHANCE 这一个开关。产物沿用
|
||||||
|
# run_pytest.sh 的目录结构,所以每个算子恰好留下两份 run.log:
|
||||||
|
#
|
||||||
|
# runs/<op>_<时间戳>/run.log 关(before)
|
||||||
|
# runs/<op>_<时间戳>/run.log 开(after)
|
||||||
|
#
|
||||||
|
# 跑完用 batch_summary.py --ab <before> <after> 得到该算子的 speedup 区间。
|
||||||
|
#
|
||||||
|
# 用法:
|
||||||
|
# OPS="softmax gelu_backward" bash ab_rlc_test.sh
|
||||||
|
# OPS="sort:perf_sort topk" bash ab_rlc_test.sh # 冒号分隔 OP:OP_FILE
|
||||||
|
#
|
||||||
|
# 注意:RLC 开关名带厂商前缀,用 VENDOR 指定(默认 MUSA)。套用前先确认开关有效:
|
||||||
|
# grep 该开关名于 FlagTree 源码应有命中,否则两侧跑的是同一份代码。
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
VENDOR="${VENDOR:-MUSA}"
|
||||||
|
ENHANCE_VAR="FLAGTREE_${VENDOR}_RLC_ENHANCE"
|
||||||
|
|
||||||
|
# 空格分隔的算子表。元素为 OP 或 OP:OP_FILE(省略时 OP_FILE=OP)。
|
||||||
|
OPS="${OPS:-softmax}"
|
||||||
|
|
||||||
|
if [[ -t 1 && -z "${NO_COLOR:-}" ]]; then
|
||||||
|
export FLAGGEMS_PERF_COLOR=always
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOG="runs/ab_rlc_$(date +%Y%m%d-%H%M%S).log"
|
||||||
|
mkdir -p runs
|
||||||
|
|
||||||
|
{
|
||||||
|
echo ">>> vendor=$VENDOR switch=$ENHANCE_VAR ops=$OPS"
|
||||||
|
for entry in $OPS; do
|
||||||
|
op="${entry%%:*}"
|
||||||
|
op_file="${entry#*:}"
|
||||||
|
[[ "$op_file" == "$entry" ]] && op_file="$op"
|
||||||
|
|
||||||
|
echo "########## $op | $ENHANCE_VAR=0 before (record) ##########"
|
||||||
|
if ! OP="$op" OP_FILE="$op_file" env "$ENHANCE_VAR=0" bash run_pytest.sh; then
|
||||||
|
echo "!!! FAILED before: $op"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
BEFORE=$(ls -dt "runs/${op}_"* | head -1)
|
||||||
|
|
||||||
|
echo "########## $op | $ENHANCE_VAR=1 after (replay $BEFORE) ##########"
|
||||||
|
if ! OP="$op" OP_FILE="$op_file" REPLAY_FROM="$BEFORE" env "$ENHANCE_VAR=1" bash run_pytest.sh; then
|
||||||
|
echo "!!! FAILED after: $op"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
AFTER=$(ls -dt "runs/${op}_"* | head -1)
|
||||||
|
|
||||||
|
echo "########## $op | summary ##########"
|
||||||
|
python batch_summary.py --ab "$BEFORE/run.log" "$AFTER/run.log" --label "$op" \
|
||||||
|
|| echo "!!! summary failed: $op"
|
||||||
|
done
|
||||||
|
echo "########## ALL DONE ##########"
|
||||||
|
} 2>&1 | tee "$LOG"
|
||||||
|
|
||||||
|
# 去掉 ANSI 转义以便 grep/diff(终端输出保留颜色)
|
||||||
|
sed -i -E $'s/\x1b\\[[0-9;]*[A-Za-z]//g' "$LOG"
|
||||||
|
|
||||||
|
echo ">>> 总 log: $(pwd)/$LOG"
|
||||||
|
echo ">>> 每算子两份 run.log 在 runs/<op>_<时间戳>/ 下,按时间先后即 before / after"
|
||||||
+71
-1
@@ -165,12 +165,82 @@ def summarize(batch_dir: Path, out_path: Optional[Path] = None) -> Path:
|
|||||||
return out_path
|
return out_path
|
||||||
|
|
||||||
|
|
||||||
|
def ab_rows(before_text: str, after_text: str) -> List[Dict[str, str]]:
|
||||||
|
"""把两次 run.log 的测量行按 (op, dtype, size_detail) 配对。
|
||||||
|
|
||||||
|
speedup 列是 gems 对 torch,不是前后对比,所以这里用 gems_lat 自己算
|
||||||
|
before/after - 1。只有两侧都 SUCCESS 且延迟为正的行才计入;对不上的行
|
||||||
|
单独返回,避免用不完整的配对报一个看起来完整的区间。
|
||||||
|
"""
|
||||||
|
def index(text: str) -> Dict[Tuple[str, str, str], float]:
|
||||||
|
out: Dict[Tuple[str, str, str], float] = {}
|
||||||
|
for row in parse_run_log(text):
|
||||||
|
if row.get("row_status") != "SUCCESS":
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
lat = float(row.get("gems_lat", ""))
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
if lat > 0:
|
||||||
|
out[(row.get("fg_op_name", ""), row.get("dtype", ""),
|
||||||
|
row.get("size_detail", ""))] = lat
|
||||||
|
return out
|
||||||
|
|
||||||
|
before, after = index(before_text), index(after_text)
|
||||||
|
paired: List[Dict[str, str]] = []
|
||||||
|
for key in sorted(before.keys() & after.keys()):
|
||||||
|
op, dtype, size = key
|
||||||
|
b, a = before[key], after[key]
|
||||||
|
paired.append({
|
||||||
|
"fg_op_name": op, "dtype": dtype, "size_detail": size,
|
||||||
|
"before_lat": f"{b:.6f}", "after_lat": f"{a:.6f}",
|
||||||
|
"speedup_percent": f"{(b / a - 1.0) * 100.0:.2f}",
|
||||||
|
})
|
||||||
|
return paired
|
||||||
|
|
||||||
|
|
||||||
|
def report_ab(before: Path, after: Path, label: str = "") -> List[Dict[str, str]]:
|
||||||
|
"""打印一个算子的前后对比:逐行 speedup 与整体区间。"""
|
||||||
|
before_text = before.read_text(encoding="utf-8", errors="replace")
|
||||||
|
after_text = after.read_text(encoding="utf-8", errors="replace")
|
||||||
|
rows = ab_rows(before_text, after_text)
|
||||||
|
name = label or (rows[0]["fg_op_name"] if rows else "?")
|
||||||
|
if not rows:
|
||||||
|
print(f"[ab] {name}: 没有可配对的测量行"
|
||||||
|
f"(before={before} after={after})")
|
||||||
|
return rows
|
||||||
|
values = [float(r["speedup_percent"]) for r in rows]
|
||||||
|
for r in rows:
|
||||||
|
print(f" {r['dtype']:<16} {float(r['speedup_percent']):+7.2f}% "
|
||||||
|
f"{r['before_lat']} -> {r['after_lat']} {r['size_detail']}")
|
||||||
|
fallback = ""
|
||||||
|
if DIRECT_NO_CUDAGRAPH_RE.search(before_text) or \
|
||||||
|
DIRECT_NO_CUDAGRAPH_RE.search(after_text):
|
||||||
|
# 回退行用的是 do_bench 口径,与 cudagraph 行不同表可比性
|
||||||
|
fallback = " [含 BENCHMARK_DIRECT_NO_CUDAGRAPH 回退行]"
|
||||||
|
print(f"[ab] {name}: {len(rows)} 行配对,speedup "
|
||||||
|
f"{min(values):+.2f}% ~ {max(values):+.2f}%{fallback}")
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
||||||
p.add_argument("batch_dir", type=Path, help="run_batch.py 的产物目录")
|
p.add_argument("batch_dir", type=Path, nargs="?",
|
||||||
|
help="run_batch.py 的产物目录")
|
||||||
p.add_argument("-o", "--output", type=Path, default=None,
|
p.add_argument("-o", "--output", type=Path, default=None,
|
||||||
help="输出 CSV(默认 <batch_dir>/summary.csv)")
|
help="输出 CSV(默认 <batch_dir>/summary.csv)")
|
||||||
|
p.add_argument("--ab", nargs=2, type=Path, metavar=("BEFORE", "AFTER"),
|
||||||
|
help="两份 run.log,按 shape 配对输出该算子的 speedup 区间")
|
||||||
|
p.add_argument("--label", default="", help="--ab 报告里的算子名")
|
||||||
args = p.parse_args()
|
args = p.parse_args()
|
||||||
|
if args.ab:
|
||||||
|
for path in args.ab:
|
||||||
|
if not path.is_file():
|
||||||
|
raise SystemExit(f"run.log 不存在: {path}")
|
||||||
|
report_ab(args.ab[0], args.ab[1], args.label)
|
||||||
|
return
|
||||||
|
if args.batch_dir is None:
|
||||||
|
raise SystemExit("需要 batch_dir,或用 --ab BEFORE AFTER")
|
||||||
if not args.batch_dir.is_dir():
|
if not args.batch_dir.is_dir():
|
||||||
raise SystemExit(f"目录不存在: {args.batch_dir}")
|
raise SystemExit(f"目录不存在: {args.batch_dir}")
|
||||||
out = summarize(args.batch_dir, args.output)
|
out = summarize(args.batch_dir, args.output)
|
||||||
|
|||||||
Reference in new issue
Block a user