forked from zhoulin/zl_bench
Compare commits
4
Commits
da22885645
...
abf9475ff3
| 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
|
||||
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
|
||||
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
|
||||
measurement unstable run-to-run). The capture itself runs under a scratch CUDA
|
||||
RNG state so that an aborted capture cannot poison the process-wide generator
|
||||
measurement unstable run-to-run). The capture itself runs under a scratch RNG
|
||||
state so that an aborted capture cannot poison the process-wide generator
|
||||
(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
|
||||
@@ -24,8 +33,33 @@ import triton.testing as _tt
|
||||
_ORIGINAL_DO_BENCH = _tt.do_bench
|
||||
|
||||
|
||||
def _is_fatal_cuda_error(exc: Exception) -> bool:
|
||||
"""True for CUDA context errors (illegal address, OOM, device-side assert):
|
||||
def _device_interface():
|
||||
"""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
|
||||
latency as success.
|
||||
|
||||
@@ -47,8 +81,12 @@ def _is_fatal_cuda_error(exc: Exception) -> bool:
|
||||
return any(m in msg for m in markers)
|
||||
|
||||
|
||||
def _warmup_before_capture(fn, warmup_ms, grad_to_none):
|
||||
"""Warm fn up before cudagraph capture.
|
||||
# Retained under its historical name; the marker list is backend independent.
|
||||
_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
|
||||
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 torch
|
||||
|
||||
def _run():
|
||||
if grad_to_none is not 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.
|
||||
_run()
|
||||
torch.cuda.synchronize()
|
||||
di.synchronize()
|
||||
# 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.
|
||||
t0 = time.perf_counter()
|
||||
_run()
|
||||
torch.cuda.synchronize()
|
||||
di.synchronize()
|
||||
dt_ms = (time.perf_counter() - t0) * 1000.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 = max(5, min(n, 200))
|
||||
for _ in range(n):
|
||||
_run()
|
||||
torch.cuda.synchronize()
|
||||
di.synchronize()
|
||||
|
||||
|
||||
def _capture_with_scratch_rng(call):
|
||||
"""Run a graph capture under a throwaway CUDA RNG state.
|
||||
def _capture_with_scratch_rng(di, call):
|
||||
"""Run a graph capture under a throwaway device RNG state.
|
||||
|
||||
torch.cuda.graph's capture_begin unconditionally registers the default CUDA
|
||||
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
|
||||
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
|
||||
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
|
||||
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()
|
||||
scratch = torch.Generator(device="cuda")
|
||||
scratch = torch.Generator(device=torch.device(gen.device))
|
||||
scratch.manual_seed(gen.initial_seed())
|
||||
gen.graphsafe_set_state(scratch)
|
||||
try:
|
||||
@@ -118,24 +163,85 @@ def _capture_with_scratch_rng(call):
|
||||
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,
|
||||
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>")
|
||||
phase = "sync" # which step rejected cudagraph, for the fallback marker
|
||||
phase = "sync" # which step rejected graph capture, for the fallback marker
|
||||
try:
|
||||
# do_bench_cudagraph runs fn on a fresh side stream without syncing
|
||||
# with the default stream first; input tensors produced just before
|
||||
# (quantize/topk in the input iter) may still be in flight, and racing
|
||||
# on them intermittently kills the context (illegal instruction from a
|
||||
# device trap on garbage indices). Sync before switching streams.
|
||||
import torch
|
||||
torch.cuda.synchronize()
|
||||
di = _device_interface()
|
||||
# The graph path runs fn on a fresh side stream without syncing with the
|
||||
# default stream first; input tensors produced just before (quantize/topk
|
||||
# in the input iter) may still be in flight, and racing on them
|
||||
# intermittently kills the context (illegal instruction from a device
|
||||
# trap on garbage indices). Sync before switching streams.
|
||||
di.synchronize()
|
||||
phase = "warmup"
|
||||
# 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"
|
||||
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,
|
||||
rep=rep,
|
||||
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:
|
||||
# Re-raise genuine device failures; anything else falls back to plain
|
||||
# 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
|
||||
reason = type(exc).__name__
|
||||
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):
|
||||
from _term_style import tag
|
||||
_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)
|
||||
@@ -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
|
||||
|
||||
|
||||
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:
|
||||
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,
|
||||
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()
|
||||
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():
|
||||
raise SystemExit(f"目录不存在: {args.batch_dir}")
|
||||
out = summarize(args.batch_dir, args.output)
|
||||
|
||||
Reference in new issue
Block a user