- fastvideo-kernel/wan22_vae_decode: the Wan2.2 (TI2V-5B) VAE decoder on sm_100a / sm_103a at 704x1248, built on first use against fastvideo-kernel's CUTLASS submodule. It frees intermediates early and carries 4 latent frames per pass: a 5 s video peaks at 20.3 GiB, down from 94.6 GiB in the kernel's first version, with the same output. - WanVAEConfig.decode_kernel routes AutoencoderKLWan.decode through it where it applies (bf16 with fp32 accumulation, about 7x faster than the fp32 decode on a B300, PSNR 61-63 dB, SSIM 0.998 against it); decode_kernel_parallel splits the decode over the sequence-parallel group (2, 4 or 8 GPUs) and broadcasts the video, with the ranks agreeing on whether every one of them could build the kernel. - PAN2's DiT runs fused Triton kernels for Q/K RMSNorm + RoPE and LayerNorm + modulation (pan2_fusions, set_fused_kernels). - PAN2PipelineConfig turns all three on (vae_decode_kernel, vae_decode_kernel_parallel, dit_fused_kernels). - VideoGenerator copies a decoded RGB video's frames to the host once instead of once per frame. - The PAN2 presets sample at guidance 2.5, the reference launcher's default since 2026-10-23.
157 lines
6.9 KiB
Python
157 lines
6.9 KiB
Python
"""The physical lower bound of a call's time, from the work it must do and what the GPUs can do per second.
|
|
|
|
A task's spec.py may define
|
|
|
|
bounds(workload) -> {"flops": float, "bytes": float, "gpus": int} optionally also "math": one of PEAKS ("f16")
|
|
|
|
lower bounds for one call of run() at that workload: the floating-point operations any correct algorithm performs
|
|
(its convolutions and matrix products) and the bytes the busiest GPU must move to or from its memory at least once,
|
|
and the GPUs the call runs on (default 1). The bound time is then
|
|
|
|
bound = max(flops / (gpus x peak FLOP/s of the chosen math), bytes / memory bandwidth)
|
|
|
|
with the peaks of the GPU it runs on at its SM clock: a call cannot be faster than the slower of the two, even with
|
|
its FLOPs spread evenly over its GPUs and nothing else to do, so the gap, measured / bound, says how much of the way
|
|
remains. The peaks are estimates, derived as SM count x SM clock x per-SM rate per clock (DEVICES), not measured; they
|
|
are the same for every solution, so gaps compare. The SM clock is the one GUILD_SM_CLOCK_MHZ names, where a task's
|
|
environment sets it, else the one DEVICES gives the GPU, else the GPU's highest SM clock: GB200 clocks are not locked,
|
|
and read low while they idle.
|
|
|
|
python tools/bounds.py [--spec spec.py] the bound of every workload on this GPU (or the reference device)
|
|
"""
|
|
|
|
import argparse
|
|
import math
|
|
import os
|
|
import subprocess
|
|
|
|
# Per compute capability: dense per-SM throughput per clock, and the memory bandwidth. Estimates from the
|
|
# architecture's published figures; the memory clock does not follow the SM clock. `sm_clock_mhz`, where given, is the
|
|
# clock to assume instead of the GPU's.
|
|
DEVICES = {
|
|
(10, 0): {
|
|
"name": "Blackwell, compute capability 10.0",
|
|
# FLOP per SM per clock, dense (no structured sparsity), fp32 accumulation.
|
|
"per_sm_clk": {
|
|
"f16": 8192, # FP16/BF16 tensor cores (tcgen05.mma kind::f16)
|
|
"tf32": 4096, # TF32 tensor cores (kind::tf32)
|
|
"f32": 256, # FP32 FMA on the CUDA cores: 128 lanes x 2
|
|
},
|
|
"bytes_per_s": 8.0e12, # HBM3e, about 8 TB/s per GPU
|
|
},
|
|
}
|
|
PEAKS = {"f16": "16-bit tensor cores", "tf32": "TF32 tensor cores", "f32": "FP32 CUDA cores"}
|
|
# Where no GPU is visible (on a laptop, say): a GB200 GPU at its highest SM clock.
|
|
REFERENCE_DEVICE = {"capability": (10, 0), "sms": 152, "clock_mhz": 2062}
|
|
|
|
|
|
def sm_clock_mhz(uuid=None):
|
|
"""The highest SM clock of the GPU with this UUID, or the reference device's."""
|
|
try:
|
|
cmd = ["nvidia-smi", "--query-gpu=clocks.max.sm", "--format=csv,noheader,nounits"]
|
|
if uuid:
|
|
cmd.insert(1, f"--id={uuid}")
|
|
out = subprocess.run(cmd, capture_output=True, text=True, timeout=20).stdout.split()
|
|
return float(out[0]) if out else REFERENCE_DEVICE["clock_mhz"]
|
|
except (OSError, ValueError, subprocess.SubprocessError):
|
|
return REFERENCE_DEVICE["clock_mhz"]
|
|
|
|
|
|
def assumed_clock_mhz():
|
|
"""The SM clock a task's environment says the bound assumes (GUILD_SM_CLOCK_MHZ), or None where it says none."""
|
|
try:
|
|
value = float(os.environ.get("GUILD_SM_CLOCK_MHZ", ""))
|
|
except ValueError:
|
|
return None
|
|
return value if value > 0 else None
|
|
|
|
|
|
def device():
|
|
"""{"capability", "sms", "clock_mhz"} of CUDA device 0, or REFERENCE_DEVICE where there is none. It asks the GPU's
|
|
properties only, which makes no CUDA context: a process that times others must hold none."""
|
|
try:
|
|
import torch
|
|
|
|
if torch.cuda.is_available():
|
|
p = torch.cuda.get_device_properties(0)
|
|
uuid = str(getattr(p, "uuid", "") or "")
|
|
uuid = uuid if not uuid or uuid.startswith(("GPU-", "MIG-")) else f"GPU-{uuid}"
|
|
capability = (p.major, p.minor)
|
|
clock = assumed_clock_mhz() or DEVICES.get(capability, {}).get("sm_clock_mhz") or sm_clock_mhz(uuid)
|
|
return {"capability": capability, "sms": p.multi_processor_count, "clock_mhz": clock}
|
|
except ImportError:
|
|
pass
|
|
return dict(REFERENCE_DEVICE)
|
|
|
|
|
|
def peaks(dev):
|
|
"""{"f16": FLOP/s, "tf32": ..., "f32": ..., "bytes_per_s": B/s} of one GPU, or None when DEVICES lacks it."""
|
|
table = DEVICES.get(tuple(dev["capability"]))
|
|
if table is None:
|
|
return None
|
|
hz = dev["clock_mhz"] * 1e6
|
|
out = {k: v * dev["sms"] * hz for k, v in table["per_sm_clk"].items()}
|
|
out["bytes_per_s"] = table["bytes_per_s"]
|
|
return out
|
|
|
|
|
|
def bound_ms(work, peak):
|
|
"""The bound time in ms of one call doing `work` ({"flops", "bytes"[, "gpus", "math"]}) on GPUs with these peaks."""
|
|
math_kind = work.get("math", "f16")
|
|
if math_kind not in PEAKS:
|
|
raise ValueError(f"bounds(): math must be one of {sorted(PEAKS)}, not {math_kind!r}")
|
|
flops, nbytes = float(work.get("flops", 0.0)), float(work.get("bytes", 0.0))
|
|
gpus = int(work.get("gpus", 1))
|
|
return 1e3 * max(flops / (gpus * peak[math_kind]), nbytes / peak["bytes_per_s"])
|
|
|
|
|
|
def describe(dev, peak):
|
|
"""One line on how the bound is computed here."""
|
|
return (f"bound = max(FLOPs / (GPUs x {peak['f16'] / 1e15:.2f} PFLOP/s) ({PEAKS['f16']}, dense), bytes / "
|
|
f"{peak['bytes_per_s'] / 1e12:.1f} TB/s), estimated for {dev['sms']} SMs per GPU at "
|
|
f"{dev['clock_mhz']:.0f} MHz")
|
|
|
|
|
|
def workload_bounds(spec, workloads, dev=None):
|
|
"""{workload: bound ms} for a spec that defines bounds(), with the device and its peaks; ({}, dev, None) when the
|
|
spec has no bounds() or the device no peak figures."""
|
|
dev = dev or device()
|
|
peak = peaks(dev)
|
|
fn = getattr(spec, "bounds", None)
|
|
if fn is None or peak is None:
|
|
return {}, dev, peak
|
|
return {w: bound_ms(fn(w), peak) for w in workloads}, dev, peak
|
|
|
|
|
|
def geomean(xs):
|
|
xs = list(xs)
|
|
return math.exp(sum(math.log(x) for x in xs) / len(xs)) if xs else math.nan
|
|
|
|
|
|
def main():
|
|
import importlib.util
|
|
|
|
here = os.path.dirname(os.path.abspath(__file__))
|
|
p = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
p.add_argument("--spec", default=os.path.join(os.environ.get("CHECKS", here), "spec.py"))
|
|
a = p.parse_args()
|
|
module_spec = importlib.util.spec_from_file_location("spec", a.spec)
|
|
spec = importlib.util.module_from_spec(module_spec)
|
|
module_spec.loader.exec_module(spec)
|
|
workloads = [w for w, e in spec.WORKLOADS.items() if e.get("timed", True)]
|
|
bounds, dev, peak = workload_bounds(spec, workloads)
|
|
if peak is None:
|
|
raise SystemExit(f"no peak figures for compute capability {dev['capability']}")
|
|
if not bounds:
|
|
raise SystemExit(f"{a.spec} defines no bounds(workload)")
|
|
print(describe(dev, peak))
|
|
for w, ms in bounds.items():
|
|
work = spec.bounds(w)
|
|
print(f"{w:<10} {ms:10.1f} ms {work['flops'] / 1e15:8.4f} PFLOP over {work.get('gpus', 1)} GPU(s) "
|
|
f"{work['bytes'] / 1e9:6.2f} GB")
|
|
print(f"geomean {geomean(bounds.values()):.1f} ms over {len(bounds)} workloads")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|