File size: 8,274 Bytes
f70ac4f | 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 | #!/usr/bin/env python
"""Paired re-timing of evaluated strategies against FFFF, prompt by prompt.
``generate_eval.py`` records each strategy's denoise latency, but when it is
re-run for *additional* strategies the FFFF reference is regenerated only for
the pixel metrics and its latency is not recorded, so the new records would be
compared against FFFF timings from a different run under different contention.
The protocol's speedup is "matched FFFF" (section 9.4): same prompt, same seed,
same conditions. This pass restores that: for every prompt it runs FFFF and
then each requested strategy back to back (denoise only, no decode, no metrics)
and writes the strategy's latency together with the FFFF latency measured
seconds earlier into the strategy's per-prompt record.
CUDA_VISIBLE_DEVICES=0 python eval/retime_eval.py --base self_forcing \
--strategies sf_taylorseer_FFxx_x1.3,sf_teacache_Fxxx_x3 \
--shard 0 --num-shards 4 --out-root eval_out
Records already carrying ``latency_source`` are skipped, so the pass resumes.
"""
import argparse
import json
import os
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
from eval.strategies import load_strategies # noqa: E402
from harness import BASES, enter_base, load_pipeline # noqa: E402
MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
SOURCE = "retime_eval: paired with FFFF in the same process, denoise only"
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--base", choices=sorted(BASES), required=True)
p.add_argument("--strategies", required=True,
help="Comma-separated strategy names, or 'all' for every "
"non-reference strategy of the base")
p.add_argument("--shard", type=int, default=0)
p.add_argument("--num-shards", type=int, default=1)
p.add_argument("--out-root", default="eval_out")
p.add_argument("--seed", type=int, default=0)
p.add_argument("--num-latent-frames", type=int, default=21)
p.add_argument("--limit", type=int, default=None)
p.add_argument("--force", action="store_true", help="Re-time records already re-timed")
p.add_argument("--redo-ratio-bounds", default=None,
help="lo,hi: also re-time records whose measured/compute speedup "
"ratio falls outside this band -- a spike that hit the strategy "
"half of the pair rather than the FFFF half")
p.add_argument("--redo-above-ms", type=float, default=None,
help="Also re-time records whose matched FFFF latency exceeds this: "
"a neighbour's job hit that pair, and the pair is only fair "
"when both halves ran under the same conditions")
return p.parse_args()
def main():
args = parse_args()
out_root = (args.out_root if os.path.isabs(args.out_root)
else os.path.join(ROOT, args.out_root))
with open(MAPPING) as f:
rows = json.load(f)["rows"]
if args.limit:
rows = rows[:args.limit]
shard_rows = rows[args.shard::args.num_shards]
all_strategies = load_strategies(args.base)
reference = all_strategies[0]
assert reference["is_reference"]
if args.strategies == "all":
wanted = [s for s in all_strategies if not s["is_reference"]]
else:
names = set(args.strategies.split(","))
wanted = [s for s in all_strategies if s["name"] in names]
missing = names - {s["name"] for s in wanted}
if missing:
sys.exit(f"unknown strategies for {args.base}: {sorted(missing)}")
print(f"base={args.base} shard {args.shard}/{args.num_shards}: "
f"{len(shard_rows)} prompts x ({reference['name']} + {len(wanted)} strategies)",
flush=True)
base = enter_base(args.base)
import torch
from utils.misc import set_seed
from cachelib import (CacheController, build_method, cached_inference, install,
parse_schedule)
torch.set_grad_enabled(False)
device = torch.device("cuda")
pipeline = load_pipeline(base)
num_steps = len(pipeline.denoising_step_list)
def record_path(strategy, row):
return os.path.join(out_root, "per_prompt", strategy,
f"{row['prompt_suite']}_{row['suite_index']:03d}.json")
def time_one(strategy, row):
kw = dict(indicator=strategy["indicator"], coefficients=strategy["coefficients"])
if strategy["method"] != "none":
kw[strategy["param"]] = strategy["value"]
first = strategy.get("first_chunk_schedule")
ctrl = CacheController(
build_method(strategy["method"], **kw), num_steps=num_steps,
forced_steps=parse_schedule(strategy.get("schedule", "FxxF"), num_steps),
first_chunk_forced_steps=(parse_schedule(first, num_steps) if first else None))
install(pipeline.generator.model, ctrl)
set_seed(args.seed)
noise = torch.randn([1, args.num_latent_frames, 16, 60, 104],
device=device, dtype=torch.bfloat16)
_, _, timing = cached_inference(pipeline, ctrl, noise, [row["extended_prompt"]],
decode=False)
return timing, ctrl.summary()
def write_json(path, payload):
tmp = path + ".tmp"
with open(tmp, "w") as f:
json.dump(payload, f, indent=2)
os.replace(tmp, path)
def needs(strategy, row):
p = record_path(strategy["name"], row)
if not os.path.exists(p):
return False # nothing to attach the timing to; generation must run first
with open(p) as f:
rec = json.load(f)
if rec.get("status") != "complete":
return False
if args.force or "latency_source" not in rec:
return True
if (args.redo_above_ms is not None
and rec.get("matched_ffff_policy_latency_ms", 0) > args.redo_above_ms):
return True
if args.redo_ratio_bounds:
lo, hi = (float(v) for v in args.redo_ratio_bounds.split(","))
d = rec["cache_diagnostics"]
compute_x = d["denoise_forwards"] / max(d["compute_equivalent_forwards"], 1e-6)
measured_x = rec["matched_ffff_policy_latency_ms"] / rec["policy_latency_ms"]
if not (lo <= measured_x / compute_x <= hi):
return True
return False
if shard_rows:
print("warm-up pass (not recorded)", flush=True)
for s in [reference] + wanted:
time_one(s, shard_rows[0])
torch.cuda.empty_cache()
t0 = time.time()
for n, row in enumerate(shard_rows):
todo = [s for s in wanted if needs(s, row)]
if not todo:
continue
ref_timing, _ = time_one(reference, row)
for s in todo:
timing, summary = time_one(s, row)
p = record_path(s["name"], row)
with open(p) as f:
rec = json.load(f)
rec["policy_latency_ms_generation_run"] = rec.get("policy_latency_ms")
rec["policy_latency_ms"] = timing["denoise_dit_ms"]
rec["excluded_context_kv_latency_ms"] = timing["context_kv_dit_ms"]
rec["matched_ffff_policy_latency_ms"] = ref_timing["denoise_dit_ms"]
rec["latency_source"] = SOURCE
# The compute fraction is deterministic; recording it again guards
# against a mismatch between the two runs.
rec["cache_diagnostics"]["compute_equivalent_forwards_retime"] = summary.get(
"compute_equivalent_forwards")
write_json(p, rec)
done = n + 1
rate = (time.time() - t0) / done
print(f"[{args.base} retime shard {args.shard}] {done}/{len(shard_rows)} "
f"({row['prompt_suite']}/{row['suite_index']:03d}) ffff={ref_timing['denoise_dit_ms']:.0f}ms "
f"{rate:.1f}s/prompt eta {(len(shard_rows) - done) * rate / 60:.0f} min",
flush=True)
print(f"retime shard {args.shard} done in {(time.time() - t0) / 60:.1f} min", flush=True)
if __name__ == "__main__":
main()
|