Introducing Sim mode - #990
Conversation
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
|
👋 Hi! Thank you for contributing. We also recommend installing prek and configuring it to check your code before every local commit. |
|
Given that we can run in eager mode and that it also exercise the full model code, what is the use case for this? |
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
for constraint checking: This mocks the fwd call (takes ~0 ms), eager CPU execution fwd calls are taking a lot of time. for perform approx: By providing a rough estimate of values for prefill and decode times, I was already able to get an approx of the vllm bench perf within 1-5%. This simulated bench takes magnitudes less time than running it for real (obviously not possible to get this data with eager on CPU) |
Makes sense to me. Thanks! |
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
There was a problem hiding this comment.
This is a very cool idea, we should use it with long runs on scheduler changes
I left a few comments. But I have a general concert with tp > 1, if it could happen that decode and prefills gets recorded too many times, one for each model runner. Have you tried running with tp > 1?
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
|
bot:test |
|
bot:bench |
maxdebayser
left a comment
There was a problem hiding this comment.
Very nice, especially test mock model code reuse. I've left a few suggestions around the code structure.
| self._ensure_file() | ||
| assert self._fp is not None | ||
| self._fp.write(json.dumps(record) + "\n") |
There was a problem hiding this comment.
Suggestion: make self_.ensure_file() a context manager
| def get_sim_state() -> SimState: | ||
| global _sim_state | ||
| if _sim_state is None: | ||
| _sim_state = SimState() | ||
| return _sim_state |
There was a problem hiding this comment.
The only place where _sim_state is referenced is the model runner, so it can me an attribute instead of a global singleton.
|
|
||
| first_token_t = token_emit_times[0] | ||
| last_token_t = token_emit_times[-1] | ||
| ttft = first_token_t - rec.virtual_arrival |
There was a problem hiding this comment.
does the ttft still capture the waiting time in the queue this way? I see that rec.virtual_arrival is set with the first apparition of the request in record_step, which is called by the model_runner.execute_model() method, meaning it corresponds to the time of first chunked prefill
There was a problem hiding this comment.
good catch, I did add this and now rec.virtual_arrival is set upon request arrival
|
|
||
| class SimState: | ||
| def __init__(self) -> None: | ||
| self.virtual_clock_seconds: float = 0.0 |
There was a problem hiding this comment.
I think we should set the virtual_clock_seconds with the real time.time() and maintain only the difference to the real time. This would allow to capture all the overheads in the engine in addition to just the prefill time and decode time, if anything becomes suboptimal outside of self.model(), it will be captured by the sim-model:
initialization:
self.virtual_clock_seconds: float = time.time()self.diff_to_real_time: float = 0.0
update in record_step():
self.diff_to_real_time += step_secondsself.virtual_clock_seconds = time.time() + self.diff_to_real_time<-- we add the diff to time.time() instead of incrementing the virtual clock directly
this would also fix the issue of missing the waiting time in the ttft: we add edit: actually this wouldn't work correctly because self.diff_to_real_time to the existing req.arrival_time value to get the virtual arrival timeself.diff_to_real_time is the current difference now, the req.arrival_time is in the past. So probably waiting time can be computed with time.time() - req.arrival_time (keep real time reference), then added to the ttft: ttft = time_last_token - time_first_chunked_prefill + waiting_time. Hope that makes sense.
… sim mode Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
|
I am currently rethinking this. please wait with further review |
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
## Description This PR allows to run benchmarks with additional custom metrics in addition to the regular TTFT, ITL values. It patches `vllm bench` to reuse the base implementation, but on top of it collects additional metrics such as the waiting time in the queue, or the waiting time in the queue or the number of chunked prefill count. Note: we can use the client patching to collect and print information from the SimModel in PR #990. Additionally the patching should also allow to inject real number of output tokens to SimModel (because as a sim model, it cannot by itself generate a realistic number of output tokens), enabling more realistic perf simulation without `--ignore-eos` and `--custom-output-len -1` parameters. ### Contributions * `sendnn-bench serve` instead of `vllm bench`. We register a custom `spyre-chat` backend that collects per-request custom metrics from SSE responses, in addition to the existing ttft, itl values * Injection of per-request sendnn results into the existing output JSON file created by `--save-detailed` * using `--describe-metrics` flag saves the sendnn metrics descriptions to a `sendnn_bench_metrics_description.txt` file * Detailed `timeline.html` which displays for each request the waiting time and individual chunked prefill times (instead of only the ttft), as well as waiting time of decodes * test for the patching and metrics collection * A claude skill to easily add new metrics. The user has to give a detailed description of the new metric, how and where to compute the value, then the skill allows Claude to know all the places that require modifications for the new metric to appear in the .json output file and printed result. It also tells how to adapt the tests to integrate the new metrics. Examples: > **/add-bench-metric** add a metric for the prefix cache hit percent, which is based on the number of chunks saved from cache hit over expected number of prefill chunks, for each request. The expected number of chunked prefill for a given request is math.ceil(request.num_prompt_tokens / self.chunk_size) > **/add-bench-metric** a list of number of left-padding blocks for the request, one value for each decode step. The list for each request should be the same length of as the decode_latencies or decode_start_times. Ignore the exisitng left_padding variables et recompute it completely: left_padding = max_num_blocks - req_num_blocks, where max_num_blocks = math.ceil(tkv / block_size), and req_num_blocks = math.ceil(req.num_computed_tokens / block_size) * Usage docs: https://vllm--1009.org.readthedocs.build/projects/spyre/en/1009/user_guide/detailed_performance_measurement.html ### Usage 1. Export `SENDNN_INFERENCE_BENCH_METRICS_ENABLED=1` to indicate to the server to collect sendnn metrics, and also set the patchings 2. Launch the server as usual: `vllm serve {model} --max-model-len {model-len} --max-num-seqs {num_seqs}` 3. Launch the benchmarking client `spyre-bench serve --model ibm-granite/granite-3.3-8b-instruct --save-result --describe-metrics ...` Output: ``` ============ Serving Benchmark Result ============ Successful requests: XXXX Failed requests: XXXX Maximum request concurrency: XXXX Benchmark duration (s): XXXX Total input tokens: XXXX Total generated tokens: XXXX Request throughput (req/s): XXXX Output token throughput (tok/s): XXXX Peak output token throughput (tok/s): XXXX Peak concurrent requests: XXXX Total token throughput (tok/s): XXXX ---------------Time to First Token---------------- Mean TTFT (ms): XXXX Median TTFT (ms): XXXX P99 TTFT (ms): XXXX P100 TTFT (ms): XXXX -----Time per Output Token (excl. 1st token)------ Mean TPOT (ms): XXXX Median TPOT (ms): XXXX P99 TPOT (ms): XXXX P100 TPOT (ms): XXXX ---------------Inter-token Latency---------------- Mean ITL (ms): XXXX Median ITL (ms): XXXX P99 ITL (ms): XXXX P100 ITL (ms): XXXX ----------------End-to-end Latency---------------- Mean E2EL (ms): XXXX Median E2EL (ms): XXXX P99 E2EL (ms): XXXX P100 E2EL (ms): XXXX ================= SenDNN Metrics ================= Total prefill chunks processed: XXXX ---------------- Queue Wait Time ----------------- Mean Queue Wait Time (ms): XXXX Median Queue Wait Time (ms): XXXX P99 Queue Wait Time (ms): XXXX P100 Queue Wait Time (ms): XXXX ------------- Chunked Prefill Count -------------- Mean Num Chunked Prefills: XXXX Median Num Chunked Prefills: XXXX P99 Num Chunked Prefills: XXXX P100 Num Chunked Prefills: XXXX ------------ Chunked Prefill Latency ------------- Mean Chunk Prefill Latency (ms): XXXX Median Chunk Prefill Latency (ms): XXXX P99 Chunk Prefill Latency (ms): XXXX P100 Chunk Prefill Latency (ms): XXXX -------------- Decode Step Latency --------------- Mean Decode Step Latency (ms): XXXX Median Decode Step Latency (ms): XXXX P99 Decode Step Latency (ms): XXXX P100 Decode Step Latency (ms): XXXX ---------------- Prefix Cache Hit ---------------- Mean Prefix Cache Hit (%): XXXX Median Prefix Cache Hit (%): XXXX P99 Prefix Cache Hit (%): XXXX P100 Prefix Cache Hit (%): XXXX -------------- Left Padding Blocks --------------- Mean Left Padding Blocks: XXXX Median Left Padding Blocks: XXXX P99 Left Padding Blocks: XXXX P100 Left Padding Blocks: XXXX ================================================== INFO SenDNN metric descriptions written to results/sendnn_bench_metrics_description.txt INFO Spyre metrics injected into results/spyre-chat-infqps-concurrency4-granite-3.3-8b-instruct-20260811-133931.json ``` ## Related Issues None ## Test Plan Test the patching works correctly, that the metrics are saved to .json file correctly and also printed correctly ```bash pytest tests/benchmarks/test_bench_metrics.py -m cpu ``` ## Checklist - [x] I have read the [contributing guidelines](https://docs.vllm.ai/projects/spyre/en/latest/contributing) - [x] My code follows the project's code style (run `bash format.sh`) - [x] I have added tests for my changes (if applicable) - [x] I have updated the documentation (if applicable) - [x] My commits include a `Signed-off-by:` line (DCO compliance) --------- Signed-off-by: Max de Bayser <mbayser@br.ibm.com> Signed-off-by: Sophie du Couédic <sop@zurich.ibm.com> Signed-off-by: Yannick Schnider <Yannick.Schnider1@ibm.com> Co-authored-by: Max de Bayser <mbayser@br.ibm.com> Co-authored-by: Yannick Schnider <Yannick.Schnider1@ibm.com>
Description
Sim-mode lets us run the full plugin stack but mocks the forward pass (no op), while still:
How it works
When
SENDNN_INFERENCE_SIM_MODE=1, the runner swapsSpyreCausalLMfor a no-opMockSpyreCausalLMand aSimStatesingleton accumulates a virtual clock alongside the real one.Each forward step charges
SENDNN_INFERENCE_SIM_PREFILL_MSorSENDNN_INFERENCE_SIM_DECODE_MSregardless of shape.When a request finishes, its virtual lifecycle (per-token timestamps, prefill/decode durations, ITLs) is written to
sim_metrics.jsonl, in roughly the same shape as the existingrequest_metrics.jsonlso analysis tooling carries over. Wall-clockrequest_metrics.jsonlwrites are skipped in sim mode since they would all be 0.Code reuse: The
MockSpyreCausalLMfrom tests/ was reused for this implementation. It has been moved into the package sendnn_inference and is imported for theInstrumentedModelRunnerin tests/.Knobs
SENDNN_INFERENCE_SIM_MODE=1: activate the feature (default 0)SENDNN_INFERENCE_SIM_PREFILL_MS: virtual cost in milliseconds per prefill chunk (default 0)SENDNN_INFERENCE_SIM_DECODE_MS: virtual cost in milliseconds per prefill chunk (default 0)