|
| 1 | +# SPDX-License-Identifier: Apache-2.0 |
| 2 | +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | +"""Profile the engine step-rate curve for the DSpark prefix scheduler. |
| 4 | +
|
| 5 | +Times the captured FULL cudagraph replays of the target verification step at |
| 6 | +every captured batch token count and emits ``dspark_sps_curve`` breakpoints, |
| 7 | +one per capture size. The scheduler linearly interpolates between |
| 8 | +breakpoints, which amortizes cudagraph padding smoothly instead of |
| 9 | +concentrating it into thresholds at capture-size boundaries. |
| 10 | +
|
| 11 | +Example: |
| 12 | + python benchmarks/profile_dspark_sps_curve.py <target-model> \\ |
| 13 | + --speculative-config '{"method": "dspark", "model": "...", ...}' \\ |
| 14 | + --engine-args '{"tensor_parallel_size": 4, "max_num_seqs": 32}' |
| 15 | +
|
| 16 | +Paste the printed ``dspark_sps_curve`` entry into --speculative-config. |
| 17 | +
|
| 18 | +Caveats: replays run on whatever (dummy) buffer state capture left behind, so |
| 19 | +data-dependent kernels (e.g. MoE routing) may be timed on unrepresentative |
| 20 | +inputs, and per-step CPU/draft overhead is modeled only through the constant |
| 21 | +``--overhead-ms``. Only the curve's shape matters to the scheduler. |
| 22 | +""" |
| 23 | + |
| 24 | +import argparse |
| 25 | +import json |
| 26 | + |
| 27 | + |
| 28 | +def _time_fullgraph_replays(worker, iters: int, warmup: int) -> dict[int, float]: |
| 29 | + """Worker-side: time FULL graph replay per batch token count (ms/step). |
| 30 | +
|
| 31 | + Runs on every TP rank via collective_rpc so the collectives captured in |
| 32 | + the graphs stay matched; every rank replays the same descs in the same |
| 33 | + sorted order. Before timing each descriptor the input buffers are |
| 34 | + refreshed into the same coherent dummy state capture used, so replays |
| 35 | + never read stale metadata. |
| 36 | + """ |
| 37 | + import torch |
| 38 | + |
| 39 | + from vllm.v1.worker.gpu.cudagraph_utils import prepare_inputs_to_capture |
| 40 | + |
| 41 | + runner = worker.model_runner |
| 42 | + mgr = runner.cudagraph_manager |
| 43 | + assert mgr is not None and mgr.graphs, ( |
| 44 | + "No FULL cudagraphs captured; run with a cudagraph_mode that captures " |
| 45 | + "FULL decode graphs." |
| 46 | + ) |
| 47 | + # Prefer varlen spec-decode descs; fall back to all captured graphs. |
| 48 | + descs = [d for d in mgr.graphs if d.max_req_tokens is not None] |
| 49 | + if not descs: |
| 50 | + descs = list(mgr.graphs.keys()) |
| 51 | + # One desc per token count: the largest request count is the most |
| 52 | + # representative shape under load. |
| 53 | + by_tokens: dict[int, object] = {} |
| 54 | + for d in descs: |
| 55 | + cur = by_tokens.get(d.num_tokens) |
| 56 | + if cur is None or (d.num_reqs or 0) > (cur.num_reqs or 0): |
| 57 | + by_tokens[d.num_tokens] = d |
| 58 | + |
| 59 | + results: dict[int, float] = {} |
| 60 | + for num_tokens in sorted(by_tokens): |
| 61 | + desc = by_tokens[num_tokens] |
| 62 | + num_reqs = desc.num_reqs or min(num_tokens, mgr.max_num_reqs) |
| 63 | + prepare_inputs_to_capture( |
| 64 | + num_reqs, |
| 65 | + num_tokens, |
| 66 | + runner.model_state, |
| 67 | + runner.input_buffers, |
| 68 | + runner.block_tables, |
| 69 | + runner.attn_groups, |
| 70 | + runner.kv_cache_config, |
| 71 | + max_req_tokens=desc.max_req_tokens, |
| 72 | + ) |
| 73 | + graph = mgr.graphs[desc] |
| 74 | + for _ in range(warmup): |
| 75 | + graph.replay() |
| 76 | + torch.accelerator.synchronize() |
| 77 | + start = torch.Event(enable_timing=True) |
| 78 | + end = torch.Event(enable_timing=True) |
| 79 | + start.record() |
| 80 | + for _ in range(iters): |
| 81 | + graph.replay() |
| 82 | + end.record() |
| 83 | + torch.accelerator.synchronize() |
| 84 | + results[num_tokens] = start.elapsed_time(end) / iters |
| 85 | + return results |
| 86 | + |
| 87 | + |
| 88 | +def curve_breakpoints( |
| 89 | + ms_per_step: dict[int, float], overhead_ms: float |
| 90 | +) -> list[list[float]]: |
| 91 | + """Convert per-capture-size step times into ``dspark_sps_curve`` |
| 92 | + breakpoints, one per capture size. The scheduler's table linearly |
| 93 | + interpolates between them (and clamps at the ends).""" |
| 94 | + return [ |
| 95 | + [size, round(1000.0 / (ms_per_step[size] + overhead_ms), 3)] |
| 96 | + for size in sorted(ms_per_step) |
| 97 | + ] |
| 98 | + |
| 99 | + |
| 100 | +def main() -> None: |
| 101 | + parser = argparse.ArgumentParser(description=__doc__) |
| 102 | + parser.add_argument("model", help="Target model (path or HF id)") |
| 103 | + parser.add_argument( |
| 104 | + "--speculative-config", |
| 105 | + required=True, |
| 106 | + help="JSON speculative config (same value you pass to vllm serve)", |
| 107 | + ) |
| 108 | + parser.add_argument( |
| 109 | + "--engine-args", |
| 110 | + default="{}", |
| 111 | + help="JSON dict of extra vllm.LLM kwargs " |
| 112 | + '(e.g. \'{"tensor_parallel_size": 4, "max_num_seqs": 32}\')', |
| 113 | + ) |
| 114 | + parser.add_argument("--iters", type=int, default=50) |
| 115 | + parser.add_argument("--warmup", type=int, default=5) |
| 116 | + parser.add_argument( |
| 117 | + "--overhead-ms", |
| 118 | + type=float, |
| 119 | + default=0.0, |
| 120 | + help="Constant per-step overhead (draft forward, sampling, CPU gap) " |
| 121 | + "added to every measured step time before converting to a rate.", |
| 122 | + ) |
| 123 | + parser.add_argument("--output", help="Write the curve JSON to this file") |
| 124 | + args = parser.parse_args() |
| 125 | + |
| 126 | + # The timing callable is shipped to the workers via collective_rpc, which |
| 127 | + # requires the pickle fallback. Local profiling tool, trusted input. |
| 128 | + import os |
| 129 | + |
| 130 | + os.environ.setdefault("VLLM_ALLOW_INSECURE_SERIALIZATION", "1") |
| 131 | + |
| 132 | + from vllm import LLM |
| 133 | + |
| 134 | + llm = LLM( |
| 135 | + model=args.model, |
| 136 | + speculative_config=json.loads(args.speculative_config), |
| 137 | + **json.loads(args.engine_args), |
| 138 | + ) |
| 139 | + per_rank = llm.collective_rpc( |
| 140 | + _time_fullgraph_replays, kwargs={"iters": args.iters, "warmup": args.warmup} |
| 141 | + ) |
| 142 | + ms_per_step = per_rank[0] |
| 143 | + |
| 144 | + print("\nMeasured FULL-graph step times (rank 0):") |
| 145 | + for size in sorted(ms_per_step): |
| 146 | + print(f" B={size:5d} tokens: {ms_per_step[size]:8.3f} ms/step") |
| 147 | + |
| 148 | + curve = curve_breakpoints(ms_per_step, args.overhead_ms) |
| 149 | + entry = {"dspark_sps_curve": curve} |
| 150 | + print("\nAdd to --speculative-config:") |
| 151 | + print(json.dumps(entry)) |
| 152 | + if args.output: |
| 153 | + with open(args.output, "w") as f: |
| 154 | + json.dump(entry, f, indent=2) |
| 155 | + print(f"\nWritten to {args.output}") |
| 156 | + |
| 157 | + |
| 158 | +if __name__ == "__main__": |
| 159 | + main() |
0 commit comments