New tool: llama-benchy - llama-bench style benchmarking for ANY LLM backend (vLLM, SGLang, llama.cpp, etc.)

Why I built this?

I’ve been happily using llama-bench to benchmark local models performance running in llama.cpp. One great feature is that it can help to evaluate performance at different context lengths and present the output in a table format that is easy to digest.

However, llama.cpp is not the only inference engine I use, I also use SGLang and vLLM. But llama-bench can only work with llama.cpp, and other benchmarking tools that I found are more focused on concurrency and total throughput.

Also, llama-bench performs measurements using the C++ engine directly which is not representative of the end user experience which can be quite different in practice.

vLLM has its own powerful benchmarking tool, but while it can be used with other inference engines, there are a few issues:

  • You can’t easily measure how prompt processing speed degrades as context grows. You can use vllm bench sweep serve, but it only works well with vLLM with prefix caching disabled on the server. Even with random prompts it will reuse the same prompt between multiple runs which will hit the cache in llama-server for instance. So you will get very low median TTFT times and very high prompt processing speeds.
  • The TTFT measurement it uses is not actually until the first usable token, it’s until the very first data chunk from the server which may not contain any generated tokens in /v1/chat/completions mode.
  • Random dataset is the only ones that allows to specify an arbitrary number of tokens, but randomly generated token sequence doesn’t let you adequately measure speculative decoding/MTP.

As of today, I haven’t been able to find any existing benchmarking tool that brings llama-bench style measurements at different context lengths to any OpenAI-compatible endpoint.

What is llama-benchy?

It’s a CLI benchmarking tool that measures:

  • Prompt Processing (pp) and Token Generation (tg) speeds at different context lengths.
  • Allows to benchmark context prefill and follow up prompt separately.
  • Reports additional metrics, like time to first response, estimated prompt processing time and end-to-end time to first token.

It works with any OpenAI-compatible endpoint that exposes /v1/chat/completions and also:

  • Supports configurable prompt length (--pp), generation length (--tg), and context depth (--depth).
  • Can run multiple iterations (--runs) and report mean ± std.
  • Uses HuggingFace tokenizers for accurate token counts.
  • Downloads a book from Project Gutenberg to use as source text for prompts to ensure better benchmarking of spec.decoding/MTP models.
  • Supports executing a command after each run (e.g., to clear cache).
  • Configurable latency measurement mode to estimate server/network overhead and provide more accurate prompt processing numbers.

Quick Demo

Benchmarking MiniMax 2.1 AWQ running on my dual Spark cluster with up to 100000 context:

# Run without installation
uvx llama-benchy --base-url http://spark:8888/v1 --model cyankiwi/MiniMax-M2.1-AWQ-4bit --depth 0 4096 8192 16384 32768 65535 100000 --adapt-prompt --latency-mode generation --enable-prefix-caching

Output:

model test t/s ttfr (ms) est_ppt (ms) e2e_ttft (ms)
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 3544.10 ± 37.29 688.41 ± 6.09 577.93 ± 6.09 688.45 ± 6.10
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 36.11 ± 0.06
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_pp @ d4096 3150.63 ± 7.84 1410.55 ± 3.24 1300.06 ± 3.24 1410.58 ± 3.24
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_tg @ d4096 34.36 ± 0.08
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 @ d4096 2562.47 ± 21.71 909.77 ± 6.75 799.29 ± 6.75 909.81 ± 6.75
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 @ d4096 33.41 ± 0.05
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_pp @ d8192 2832.52 ± 12.34 3002.66 ± 12.57 2892.18 ± 12.57 3002.70 ± 12.57
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_tg @ d8192 31.38 ± 0.06
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 @ d8192 2261.83 ± 10.69 1015.96 ± 4.29 905.48 ± 4.29 1016.00 ± 4.29
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 @ d8192 30.55 ± 0.08
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_pp @ d16384 2473.70 ± 2.15 6733.76 ± 5.76 6623.28 ± 5.76 6733.80 ± 5.75
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_tg @ d16384 27.89 ± 0.04
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 @ d16384 1824.55 ± 6.32 1232.96 ± 3.89 1122.48 ± 3.89 1233.00 ± 3.89
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 @ d16384 27.21 ± 0.04
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_pp @ d32768 2011.11 ± 2.40 16403.98 ± 19.43 16293.50 ± 19.43 16404.03 ± 19.43
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_tg @ d32768 22.09 ± 0.07
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 @ d32768 1323.21 ± 4.62 1658.25 ± 5.41 1547.77 ± 5.41 1658.29 ± 5.41
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 @ d32768 21.81 ± 0.07
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_pp @ d65535 1457.71 ± 0.26 45067.98 ± 7.94 44957.50 ± 7.94 45068.01 ± 7.94
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_tg @ d65535 15.72 ± 0.04
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 @ d65535 840.36 ± 2.35 2547.54 ± 6.79 2437.06 ± 6.79 2547.60 ± 6.80
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 @ d65535 15.63 ± 0.02
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_pp @ d100000 1130.05 ± 1.89 88602.31 ± 148.70 88491.83 ± 148.70 88602.37 ± 148.70
cyankiwi/MiniMax-M2.1-AWQ-4bit ctx_tg @ d100000 12.14 ± 0.02
cyankiwi/MiniMax-M2.1-AWQ-4bit pp2048 @ d100000 611.01 ± 2.50 3462.39 ± 13.73 3351.90 ± 13.73 3462.42 ± 13.73
cyankiwi/MiniMax-M2.1-AWQ-4bit tg32 @ d100000 12.05 ± 0.03

llama-benchy (0.1.0)
date: 2026-01-06 11:44:49 | latency mode: generation

GitHub

https://github.com/eugr/llama-benchy

Thanks for one more amazing contribution @eugr

New major update is out - v0.2.0.

It brings:

  • concurrency support
  • added JSON and CSV outputs. JSON output contains values for individual runs in addition to mean/std values.
  • ability to save results to a file

Example of concurrency testing. This is for model running on a single Spark with the following parameters:

  • gpu_memory_utilization: 0.7
  • max_model_len: 202752
  • max_num_batched_tokens: 4096
  • max_num_seqs: 64
llama-benchy \
  --base-url http://spark:8888/v1 \
  --model cyankiwi/GLM-4.7-Flash-AWQ-4bit \
  --served-model-name glm-4.7-flash \
  --depth 0 4096 \
  --adapt-prompt \
  --concurrency 1 2 10

Maximum supported concurrency is:

GPU KV cache size: 1,239,088 tokens
Maximum concurrency for 202,752 tokens per request: 6.11x
model test t/s (total) t/s (req) ttfr (ms) est_ppt (ms) e2e_ttft (ms)
cyankiwi/GLM-4.7-Flash-AWQ-4bit pp2048 (c1) 5326.13 ± 38.60 5326.13 ± 38.60 336.01 ± 5.86 331.75 ± 5.86 336.14 ± 5.82
cyankiwi/GLM-4.7-Flash-AWQ-4bit tg32 (c1) 41.75 ± 0.03 41.75 ± 0.03
cyankiwi/GLM-4.7-Flash-AWQ-4bit pp2048 (c2) 5772.72 ± 310.43 2953.01 ± 141.62 632.95 ± 22.99 628.68 ± 22.99 633.00 ± 22.99
cyankiwi/GLM-4.7-Flash-AWQ-4bit tg32 (c2) 73.74 ± 1.31 37.38 ± 0.63
cyankiwi/GLM-4.7-Flash-AWQ-4bit pp2048 (c10) 6118.58 ± 28.10 897.80 ± 400.77 2336.27 ± 705.73 2332.00 ± 705.73 2336.31 ± 705.72
cyankiwi/GLM-4.7-Flash-AWQ-4bit tg32 (c10) 87.65 ± 2.76 15.33 ± 4.11
cyankiwi/GLM-4.7-Flash-AWQ-4bit pp2048 @ d4096 (c1) 5066.12 ± 9.45 5066.12 ± 9.45 1097.99 ± 18.53 1093.72 ± 18.53 1098.06 ± 18.53
cyankiwi/GLM-4.7-Flash-AWQ-4bit tg32 @ d4096 (c1) 39.27 ± 0.11 39.27 ± 0.11
cyankiwi/GLM-4.7-Flash-AWQ-4bit pp2048 @ d4096 (c2) 5274.22 ± 15.83 2665.63 ± 38.23 2042.41 ± 37.06 2038.14 ± 37.06 2042.50 ± 37.06
cyankiwi/GLM-4.7-Flash-AWQ-4bit tg32 @ d4096 (c2) 68.02 ± 0.32 34.73 ± 0.66
cyankiwi/GLM-4.7-Flash-AWQ-4bit pp2048 @ d4096 (c10) 5217.03 ± 15.20 1051.40 ± 608.75 6666.76 ± 2782.40 6662.49 ± 2782.40 6666.81 ± 2782.39
cyankiwi/GLM-4.7-Flash-AWQ-4bit tg32 @ d4096 (c10) 31.33 ± 0.21 8.14 ± 5.03

llama-benchy (0.2.0)
date: 2026-02-05 15:49:22 | latency mode: api

Please note that that t/s (total) is measured for the entire concurrent batch run, and does not indicate peak throughput. Peak throughput will be added in the next release.

Very nice!

BTW are you familiar with aiperf?

Playing around with it lately. May a source of inspiration.

Yes, I’ve looked at it, as well as a few others, but it’s closer to vllm bench serve - lots of functionality, but I wanted to create something like llama-bench, but for all backends, so we could see the effect of context on follow up requests. And have a simple to digest output.

Thank you for the share, eugr. I’m curious if the oss-120b benchmark results on GitHub were obtained on a single DGX Spark setup?

It’s a mix. Most were dual Sparks, with the exception of concurrency example which was for a single Spark. The numbers for dual sparks are lower than they should be, I was running some other stuff on those sparks at that time, and it slowed things down a bit. On a good day I see up to 77-78 t/s on single requests with low context.

Feel free to check latest numbers for some models here as well

PSA: llama-benchy will now try to infer HF model name (and served-model-name if applicable) from the endpoint if --model parameter is not specified. Works with vLLM and SGLang.

While it works with llama.cpp if the server was launched with -hf parameter (passing HF model name), depending on the GGUF model repository, it may not be able to load a tokenizer. In this case you can pass HF model name in non-GGUF format that has a tokenizer in the repository. It will still work, but may report slightly incorrect results depending on how different is the tokenizer from the default gpt2 one.

thanks for doing what you are doing @eugr , more helpful than nvidia staff - hope they send you some sparks for free as a thank you at least

I want to batch benchmark all my LLMs and use lama-benchy for it.

Most are served by vLLM some are lama.cpp and I have not looked into SGLang yet.
The setup is liteLLM → Lama-Swap → vLLM or lama.cpp

Guess there is already a batch benchmark setting in lama-benchy but I could not find it. So I have let the AI write a Benchmark batch script for me.. it works.. you just need to change the lama-swap destination http://localhost:28080 to the one you use.

benchmark-models.sh — Benchmark all llama-swap models using llama-benchy

Uses llama-benchy (GitHub - eugr/llama-benchy: llama-benchy - llama-bench style benchmarking tool for all backends · GitHub) for standardized
LLM performance measurement. Results are comparable with other DGX Spark
users who use the same tool.

What it measures:
pp (prompt processing) = how fast the model reads your input (tok/s)
tg (token generation) = how fast the model writes its reply (tok/s)
TTFT (time to first token) = how long before you see the first word

Usage:
./benchmark-models.sh # Benchmark all models
./benchmark-models.sh Qwen3.5 Nemotron # Only models matching names
./benchmark-models.sh --quick Nemotron # Fast single-run test
./benchmark-models.sh --full # Full sweep with depths
./benchmark-models.sh --runs 5 Qwen3.5-35B # Custom run count

the command line output looks like this

============================================================
  DGX Spark Model Benchmark
  powered by llama-benchy (github.com/eugr/llama-benchy)
============================================================

  Endpoint : http://localhost:28080
  Mode     : standard
  Settings : pp=512 2048  tg=128 256  depth=0  runs=3
  Date     : 2026-04-15 12:26
  Results  : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks

  pp = prompt processing (how fast the model reads your input)
  tg = token generation  (how fast the model writes its reply)

Found 15 model(s) to benchmark.

============================================================
  [1/15] GPT-OSS-120B
============================================================

  Loading model via llama-swap...
  Model ready (loaded in 209.411370441s)
  Profile: Medium Log (50-page document baseline)
  Running llama-benchy (pp=2048  tg=128  depth=0 16384  runs=3)...

  llama-benchy results (copy this to share on forums):

| model        |            test |             t/s |     peak t/s |        ttfr (ms) |     est_ppt (ms) |    e2e_ttft (ms) |
|:-------------|----------------:|----------------:|-------------:|-----------------:|-----------------:|-----------------:|
| GPT-OSS-120B |          pp2048 | 3468.07 ± 76.96 |              |   577.07 ± 17.35 |   528.72 ± 17.35 |   690.45 ± 23.61 |
| GPT-OSS-120B |           tg128 |    27.67 ± 0.07 | 29.00 ± 0.00 |                  |                  |                  |
| GPT-OSS-120B | pp2048 @ d16384 | 2781.32 ± 87.36 |              | 5959.68 ± 157.80 | 5911.33 ± 157.80 | 6081.95 ± 154.08 |
| GPT-OSS-120B |  tg128 @ d16384 |    25.02 ± 0.74 | 26.00 ± 0.82 |                  |                  |                  |
  Context Depth Analysis:

  --- depth=0 — Baseline (no context) ---
    Reading speed:  3,899 +/- 30 tok/s
    Writing speed:  27.8 tok/s  (peak: 29)
      -> Fast — comfortable for interactive chat
    Time to first token:  686ms
      -> Short pause before response starts

  --- depth=16,384 — Medium Log (~50 pages) ---
    Reading speed:  2,981 +/- 55 tok/s
    Writing speed:  25.8 tok/s  (peak: 27)
      -> -7.3% vs baseline (moderate slowdown)
      -> Fast — comfortable for interactive chat
    Time to first token:  5678ms
      -> Noticeable wait (5.7s)

  Overall impact: depth 0 -> 16,384 = -7.3% generation speed
  Conclusion: Model handles deep context well on this hardware

  JSON data : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/GPT-OSS-120B_20260416_095536.json
  Forum table: /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/GPT-OSS-120B_20260416_095536.md

the GPT-OSS-120B_20260415_122622.md file content looks like this

| model        |   test |              t/s |     peak t/s |      ttfr (ms) |   est_ppt (ms) |   e2e_ttft (ms) |
|:-------------|-------:|-----------------:|-------------:|---------------:|---------------:|----------------:|
| GPT-OSS-120B |  pp512 |  1888.57 ± 21.46 |              |  283.45 ± 5.95 |  234.77 ± 5.95 |   384.97 ± 9.69 |
| GPT-OSS-120B |  tg128 |     31.26 ± 0.07 | 32.00 ± 0.00 |                |                |                 |
| GPT-OSS-120B |  pp512 | 1956.00 ± 122.30 |              |  293.77 ± 6.04 |  245.09 ± 6.04 |   395.22 ± 6.88 |
| GPT-OSS-120B |  tg256 |     31.16 ± 0.07 | 32.00 ± 0.00 |                |                |                 |
| GPT-OSS-120B | pp2048 | 3936.35 ± 107.89 |              |  513.56 ± 6.30 |  464.88 ± 6.30 |  615.79 ± 10.97 |
| GPT-OSS-120B |  tg128 |     30.94 ± 0.06 | 32.00 ± 0.00 |                |                |                 |
| GPT-OSS-120B | pp2048 |  3937.06 ± 25.26 |              | 512.60 ± 15.64 | 463.92 ± 15.64 |  616.41 ± 15.08 |
| GPT-OSS-120B |  tg256 |     30.79 ± 0.06 | 32.00 ± 0.00 |                |                |                 |

at the end you get a summary

============================================================
  BENCHMARK REPORT — DGX Spark
============================================================

  Date: 2026-04-16 17:46  |  Mode: medium-log  |  Runs: 3
  Depths tested: 0 16384
  Models tested: 15  |  Passed: 15  Failed: 0
  Total benchmark time: 77.8 min

  Model                                            Read (pp)    Write (tg)      Peak      TTFT        Deep ctx
                                                       tok/s         tok/s     tok/s        ms     degradation
  ----------------------------------------------------------------------------------------------------------
  GPT-OSS-120B                                    4356 +/-46          32.3        33       595      -8.4% @16k
  Mistral-Small-24B-Instruct-2501                1955 +/-188           4.6         5      1166      -3.7% @16k
  Nemotron-3-Nano-30B-A3B-NVFP4                  8392 +/-267   42.1 +/-1.7        45       290             —
  Nemotron-3-Nano-4B-FP8                        7077 +/-1566          38.8        40       340             —
  Nemotron-3-Nano-4B-Q4_K_M-GGUF                  2783 +/-76          49.7        51       760             —
  Nemotron-3-Super-120B-A12B-NVFP4                1725 +/-37          14.8        16      1221      -1.6% @16k
  Qwen3-Coder-Next-FP8-Dynamic                   1986 +/-806   32.0 +/-1.2        34      1278      -0.4% @16k
  Qwen3-Coder-Next-int4-AutoRound                3268 +/-627   66.3 +/-1.1        68       644      -3.6% @16k
  Qwen3-Omni-30B-A3B-Instruct                   4445 +/-1426          30.3        32       584     -15.0% @16k
  Qwen3-VL-30B-A3B-Instruct-FP8                 6335 +/-3014          51.5        53       508     -12.2% @16k
  Qwen3.5-122B-A10B-int4-AutoRound               1760 +/-266          25.6        27      1210      -2.3% @16k
  Qwen3.5-35B-A3B-FP8                           4166 +/-1748          21.2        22       689      -0.5% @16k
  Qwen3.5-35B-A3B-Uncensored-HauhauCS-Agg...      1742 +/-39          56.7        59      1160             —
  TheDrummer_GLM-Steam-106B-A12B-v1-Q4_K_...        538 +/-5          16.5        17      3443             —
  ikiru/dolphin-mistral-24b-venice-editio...      1512 +/-19          13.6        14      2115     -12.6% @16k

  ---------------------------------------------------------------
  How to read this table:
  
    Read (pp)    = How fast the model reads your prompt (higher = better).
    Write (tg)   = How fast the model types its answer at depth=0 baseline.
                    This is the speed you feel when chatting.
                    Humans read at ~4 tok/s, so 20+ feels smooth.
    Peak         = Fastest burst speed observed in a 1-second window.
    TTFT         = Time until the first word appears (lower = better).
    Deep ctx     = Speed change at max tested depth vs baseline.
                    >-15% = unified memory bandwidth bottleneck.
  ---------------------------------------------------------------

  Results saved to:
    Report     : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/report_20260416_162852.txt
    JSON data  : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/*_20260416_162852.json
    Forum tables: /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/*_20260416_162852.md

here is the script

benchmark-models.sh
# =============================================================================
# benchmark-models.sh — Benchmark all llama-swap models using llama-benchy
#
# Uses llama-benchy (https://github.com/eugr/llama-benchy) for standardized
# LLM performance measurement. Results are comparable with other DGX Spark
# users who use the same tool.
#
# Benchmark profiles simulate real-world document analysis workloads:
#
#   "Medium Log"  (default)  — 50-page document, pp2048 + tg128 @ depth 16384
#   "Massive Log" (--stress) — 100+ page log,    pp2048 + tg128 @ depth 32768
#   "Extreme"     (--extreme)— 200+ page corpus,  pp2048 + tg128 @ depth 65535
#
# Each profile includes a depth=0 baseline so you can see the performance
# delta as unified memory pressure increases.
#
# Usage:
#   ./benchmark-models.sh                        # Medium Log (default)
#   ./benchmark-models.sh --stress               # Medium + Massive Log
#   ./benchmark-models.sh --extreme              # All three depth levels
#   ./benchmark-models.sh --quick Nemotron       # Fast smoke test
#   ./benchmark-models.sh --runs 5 Qwen3.5-35B   # Custom run count
#   ./benchmark-models.sh Qwen3.5 Nemotron       # Only matching models
# =============================================================================

set -euo pipefail

LLAMA_SWAP_URL="${LLAMA_SWAP_URL:-http://localhost:28080}"
RESULTS_DIR="$(dirname "$(readlink -f "$0")")/test-results/benchmarks"
TIMEOUT=1800

# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# Defaults: "Medium Log" baseline
PP="2048"
TG="128"
DEPTH="0 16384"
RUNS=3
MODE="medium-log"
FILTERS=()

# Parse arguments
while [[ $# -gt 0 ]]; do
    case "$1" in
        --quick)
            PP="512"
            TG="128"
            DEPTH="0"
            RUNS=1
            MODE="quick"
            shift
            ;;
        --stress)
            # Medium Log + Massive Log
            PP="2048"
            TG="128"
            DEPTH="0 16384 32768"
            RUNS=3
            MODE="stress"
            shift
            ;;
        --extreme)
            # All three: Medium + Massive + Extreme limit
            PP="2048"
            TG="128"
            DEPTH="0 16384 32768 65535"
            RUNS=3
            MODE="extreme"
            shift
            ;;
        --full)
            # Comprehensive sweep (original broad test)
            PP="512 2048"
            TG="128 256 512"
            DEPTH="0 16384 32768"
            RUNS=3
            MODE="full"
            shift
            ;;
        --runs)
            RUNS="$2"
            shift 2
            ;;
        --runs=*)
            RUNS="${1#*=}"
            shift
            ;;
        --help|-h)
            cat <<'HELPEOF'
Usage: benchmark-models.sh [OPTIONS] [FILTER...]

Benchmark your llama-swap models using llama-benchy.
Results use the standard llama-benchy format for comparison
with other DGX Spark users on the NVIDIA forums.

Profiles (simulating real-world document workloads):

  (default)    "Medium Log" — pp2048, tg128 @ depth 0 + 16384
               Simulates a ~50-page document. The depth=0 baseline shows
               raw speed; depth=16384 shows the cost of a full KV cache.

  --stress     "Massive Log" — adds depth 32768
               Doubles the context to simulate a massive error log.
               Watch for tg tok/s drop vs. the medium baseline — that's
               where shared memory bandwidth starts to bottleneck.

  --extreme    "Extreme Limit" — adds depth 65535
               Pushes to ~100 pages of text. Strictly to see if the
               system can process it without crashing or heavy swap.

  --quick      Smoke test — pp512, tg128, depth 0, 1 run
  --full       Broad sweep — pp512+2048, tg128+256+512, depths 0-32k

Other options:
  --runs N     Override number of runs (default: 3)
  --help       Show this help

Filters:
  Add model name fragments to only test matching models.
  Example: ./benchmark-models.sh --stress Qwen3.5 Nemotron

Environment:
  LLAMA_SWAP_URL  llama-swap endpoint (default: http://localhost:28080)

What the numbers mean:
  pp tok/s  = Prompt Processing speed. How fast the model reads your input.
              Higher is better. Typically 500-5000+ tok/s on DGX Spark.
  tg tok/s  = Token Generation speed. How fast the model writes its reply.
              Higher is better. This is the number you "feel" when chatting.
              Typically 15-50+ tok/s on DGX Spark depending on model size.
  TTFT      = Time To First Token. The delay before the model starts replying.
              Lower is better. Measured in milliseconds.

Key insight:
  Compare tg tok/s across depths. A big drop from depth=16384 to depth=32768
  means you've found the unified memory bandwidth bottleneck between the ARM
  CPU and the Blackwell GPU on DGX Spark.
HELPEOF
            exit 0
            ;;
        *)
            FILTERS+=("$1")
            shift
            ;;
    esac
done

mkdir -p "$RESULTS_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_FILE="$RESULTS_DIR/report_${TIMESTAMP}.txt"

log() { echo -e "$1" | tee -a "$REPORT_FILE"; }

unload_all() {
    curl -sf -X POST "$LLAMA_SWAP_URL/unload" > /dev/null 2>&1 || true
    sleep 5
}

# Warm up: send a tiny request to make llama-swap load the model
warmup_model() {
    local model="$1"
    log "  Loading model via llama-swap..."
    local start end elapsed
    start=$(date +%s.%N)

    local response
    response=$(curl -s --max-time "$TIMEOUT" \
        -X POST "$LLAMA_SWAP_URL/v1/chat/completions" \
        -H "Content-Type: application/json" \
        -d "$(jq -n --arg model "$model" '{
            model: $model,
            messages: [{role: "user", content: "Say OK"}],
            max_tokens: 5
        }')" 2>&1) || true

    end=$(date +%s.%N)
    elapsed=$(echo "scale=1; $end - $start" | bc)

    local err
    err=$(echo "$response" | jq -r '.error.message // empty' 2>/dev/null)
    if [[ -n "$err" ]]; then
        log "  ${RED}FAILED to load: $err${NC}"
        return 1
    fi

    log "  Model ready (loaded in ${elapsed}s)"
    return 0
}

# Run llama-benchy and capture results
run_benchy() {
    local model="$1"
    local safe_name="${model//\//_}"
    local json_file="$RESULTS_DIR/${safe_name}_${TIMESTAMP}.json"
    local md_file="$RESULTS_DIR/${safe_name}_${TIMESTAMP}.md"

    # Show profile description
    case "$MODE" in
        medium-log) log "  Profile: Medium Log (50-page document baseline)" ;;
        stress)     log "  Profile: Massive Log (stress test — watch for bandwidth bottleneck)" ;;
        extreme)    log "  Profile: Extreme Limit (push to ~100 pages, crash/swap detection)" ;;
        quick)      log "  Profile: Quick smoke test" ;;
        full)       log "  Profile: Full comprehensive sweep" ;;
    esac
    log "  Running llama-benchy (pp=$PP  tg=$TG  depth=$DEPTH  runs=$RUNS)..."
    log ""

    # --- Run 1: Save JSON for data parsing ---
    local cmd_json="uvx llama-benchy"
    cmd_json+=" --base-url $LLAMA_SWAP_URL/v1"
    cmd_json+=" --model $model"
    cmd_json+=" --pp $PP"
    cmd_json+=" --tg $TG"
    cmd_json+=" --depth $DEPTH"
    cmd_json+=" --runs $RUNS"
    cmd_json+=" --latency-mode generation"
    cmd_json+=" --no-warmup"
    cmd_json+=" --skip-coherence"
    cmd_json+=" --save-result ${json_file}"
    cmd_json+=" --format json"

    local output exit_code=0
    output=$(eval "$cmd_json" 2>&1) || exit_code=$?

    if [[ $exit_code -ne 0 ]]; then
        log "  ${RED}llama-benchy failed:${NC}"
        echo "$output" | tail -15 | tee -a "$REPORT_FILE"
        return 1
    fi

    # --- Run 2: Get the markdown table (for sharing on forums) ---
    local cmd_md="uvx llama-benchy"
    cmd_md+=" --base-url $LLAMA_SWAP_URL/v1"
    cmd_md+=" --model $model"
    cmd_md+=" --pp $PP"
    cmd_md+=" --tg $TG"
    cmd_md+=" --depth $DEPTH"
    cmd_md+=" --runs $RUNS"
    cmd_md+=" --latency-mode generation"
    cmd_md+=" --no-warmup"
    cmd_md+=" --skip-coherence"
    cmd_md+=" --save-result ${md_file}"
    cmd_md+=" --format md"

    local md_output
    md_output=$(eval "$cmd_md" 2>&1) || true

    # Show the full saved markdown file (the format people share on forums)
    if [[ -f "$md_file" ]]; then
        log "  ${CYAN}llama-benchy results (copy this to share on forums):${NC}"
        log ""
        cat "$md_file" | tee -a "$REPORT_FILE"
        log ""
    else
        # Fallback: show table lines from stdout if file wasn't created
        local table_lines
        table_lines=$(echo "$md_output" | grep -E '^\|')
        if [[ -n "$table_lines" ]]; then
            log "  ${CYAN}llama-benchy results:${NC}"
            log ""
            echo "$table_lines" | tee -a "$REPORT_FILE"
            log ""
        fi
    fi

    # --- Parse JSON and show friendly explanation ---
    if [[ -f "$json_file" ]]; then
        python3 <<PYEOF | tee -a "$REPORT_FILE"
import json

with open('$json_file') as f:
    data = json.load(f)

benchmarks = data.get('benchmarks', [])
if not benchmarks:
    print("  (no benchmark data found)")
    exit(0)

# Check if all benchmarks have null results (model failed silently)
all_null = all(
    b.get('pp_throughput') is None and b.get('tg_throughput') is None
    for b in benchmarks
)
if all_null:
    print("  (model returned no usable results — it may not support this benchmark)")
    exit(0)

# Group benchmarks by depth for comparison
depth_results = {}
for b in benchmarks:
    depth = b.get('context_size', 0)
    pp_obj  = b.get('pp_throughput') or {}
    tg_obj  = b.get('tg_throughput') or {}
    pk_obj  = b.get('peak_throughput') or {}
    e2e_obj = b.get('e2e_ttft') or {}
    pp_mean  = pp_obj.get('mean', 0) or 0
    tg_mean  = tg_obj.get('mean', 0) or 0
    if pp_mean > 0 or tg_mean > 0:
        depth_results[depth] = {
            'pp_mean': pp_mean, 'pp_std': (pp_obj.get('std', 0) or 0),
            'tg_mean': tg_mean, 'tg_std': (tg_obj.get('std', 0) or 0),
            'pk_mean': (pk_obj.get('mean', 0) or 0),
            'e2e_mean': (e2e_obj.get('mean', 0) or 0),
        }

# Depth label mapping
depth_labels = {
    0: "Baseline (no context)",
    16384: "Medium Log (~50 pages)",
    32768: "Massive Log (~100 pages)",
    65535: "Extreme Limit (~200 pages)",
}

print("  Context Depth Analysis:")
print("")

baseline_tg = None
for depth in sorted(depth_results.keys()):
    r = depth_results[depth]
    label = depth_labels.get(depth, f"depth {depth}")
    pp_mean, pp_std = r['pp_mean'], r['pp_std']
    tg_mean, tg_std = r['tg_mean'], r['tg_std']
    pk_mean = r['pk_mean']
    e2e_mean = r['e2e_mean']

    print(f"  --- depth={depth:,} — {label} ---")

    # Reading speed
    if pp_mean > 0:
        if pp_std > 0.5:
            print(f"    Reading speed:  {pp_mean:,.0f} +/- {pp_std:,.0f} tok/s")
        else:
            print(f"    Reading speed:  {pp_mean:,.0f} tok/s")

    # Writing speed
    if tg_mean > 0:
        if tg_std > 0.5:
            print(f"    Writing speed:  {tg_mean:.1f} +/- {tg_std:.1f} tok/s  (peak: {pk_mean:.0f})")
        else:
            print(f"    Writing speed:  {tg_mean:.1f} tok/s  (peak: {pk_mean:.0f})")

        # Show degradation from baseline
        if baseline_tg is None:
            baseline_tg = tg_mean
        elif baseline_tg > 0:
            pct = ((tg_mean - baseline_tg) / baseline_tg) * 100
            if pct < -15:
                print(f"    {chr(9888)}  {pct:+.1f}% vs baseline — BANDWIDTH BOTTLENECK DETECTED")
            elif pct < -5:
                print(f"      -> {pct:+.1f}% vs baseline (moderate slowdown)")
            else:
                print(f"      -> {pct:+.1f}% vs baseline (minimal impact)")

        # What it feels like
        if tg_mean >= 40:
            feel = "Very fast — feels instant, smooth streaming"
        elif tg_mean >= 25:
            feel = "Fast — comfortable for interactive chat"
        elif tg_mean >= 15:
            feel = "Good — readable streaming with slight pauses"
        elif tg_mean >= 8:
            feel = "Moderate — noticeable wait, but usable"
        else:
            feel = "Slow — may feel sluggish for chat"
        print(f"      -> {feel}")

    # TTFT
    if e2e_mean > 0:
        print(f"    Time to first token:  {e2e_mean:.0f}ms")
        if e2e_mean < 200:
            print(f"      -> Feels instant")
        elif e2e_mean < 500:
            print(f"      -> Barely noticeable delay")
        elif e2e_mean < 2000:
            print(f"      -> Short pause before response starts")
        else:
            print(f"      -> Noticeable wait ({e2e_mean/1000:.1f}s)")
    print("")

# Summary comparison if multiple depths
if len(depth_results) > 1 and baseline_tg and baseline_tg > 0:
    max_depth = max(depth_results.keys())
    deepest_tg = depth_results[max_depth]['tg_mean']
    total_pct = ((deepest_tg - baseline_tg) / baseline_tg) * 100
    print(f"  Overall impact: depth 0 -> {max_depth:,} = {total_pct:+.1f}% generation speed")
    if total_pct < -20:
        print(f"  Conclusion: Significant unified memory bandwidth bottleneck at depth {max_depth:,}")
    elif total_pct < -10:
        print(f"  Conclusion: Moderate bandwidth pressure — usable but noticeably slower")
    else:
        print(f"  Conclusion: Model handles deep context well on this hardware")
    print("")

PYEOF
    fi

    log "  ${DIM}JSON data : $json_file${NC}"
    log "  ${DIM}Forum table: $md_file${NC}"
    return 0
}

# --------------- MAIN ---------------
log ""
log "${BOLD}============================================================${NC}"
log "${BOLD}  DGX Spark Model Benchmark${NC}"
log "${BOLD}  powered by llama-benchy (github.com/eugr/llama-benchy)${NC}"
log "${BOLD}============================================================${NC}"
log ""
log "  Endpoint : $LLAMA_SWAP_URL"
log "  Mode     : $MODE"
log "  Settings : pp=$PP  tg=$TG  depth=$DEPTH  runs=$RUNS"
log "  Date     : $(date '+%Y-%m-%d %H:%M')"
log "  Results  : $RESULTS_DIR"
log ""

# Mode descriptions
case "$MODE" in
    medium-log)
        log "  ${CYAN}Profile: Medium Log Baseline${NC}"
        log "  ${DIM}Simulates a ~50-page document (depth=16384). Establishes your${NC}"
        log "  ${DIM}baseline generation speed with a moderately full KV cache.${NC}"
        ;;
    stress)
        log "  ${CYAN}Profile: Massive Log Stress Test${NC}"
        log "  ${DIM}Doubles context to simulate a massive error log (depth=32768).${NC}"
        log "  ${DIM}Watch for tg tok/s drop — that's the unified memory bottleneck.${NC}"
        ;;
    extreme)
        log "  ${CYAN}Profile: Extreme Limit Test${NC}"
        log "  ${DIM}Pushes to ~200 pages (depth=65535). Tests if the system can${NC}"
        log "  ${DIM}process it without crashing or heavy swap paging.${NC}"
        ;;
    quick)
        log "  ${DIM}Quick smoke test — just checking if models respond.${NC}"
        ;;
    full)
        log "  ${DIM}Full comprehensive sweep — broad pp/tg/depth combinations.${NC}"
        ;;
esac
log ""
log "  ${DIM}pp = prompt processing (how fast the model reads your input)${NC}"
log "  ${DIM}tg = token generation  (how fast the model writes its reply)${NC}"
log "  ${DIM}depth = pre-filled context tokens (simulates document size)${NC}"
log ""

# Check llama-benchy is available
if ! uvx llama-benchy --help > /dev/null 2>&1; then
    log "${RED}Error: llama-benchy not available via uvx.${NC}"
    log "Install with: pip install llama-benchy  OR  uv pip install llama-benchy"
    exit 1
fi

# Fetch model list
MODELS=$(curl -sf "$LLAMA_SWAP_URL/v1/models" | jq -r '.data[].id' | sort)
MODEL_COUNT=$(echo "$MODELS" | wc -l)

# Apply filters
if [[ ${#FILTERS[@]} -gt 0 ]]; then
    log "Filtering models matching: ${FILTERS[*]}"
    FILTERED=""
    for m in $MODELS; do
        for f in "${FILTERS[@]}"; do
            if [[ "$m" == *"$f"* ]]; then
                FILTERED="${FILTERED}${m}\n"
            fi
        done
    done
    MODELS=$(echo -e "$FILTERED" | grep -v '^$' | sort -u)
    MODEL_COUNT=$(echo "$MODELS" | wc -l)
fi

log "Found ${BOLD}${MODEL_COUNT}${NC} model(s) to benchmark."
log ""

PASS=0
FAIL=0
IDX=0
TOTAL_START=$(date +%s.%N)

# Collect results for final summary
declare -A SUMMARY_PP SUMMARY_TG SUMMARY_PEAK SUMMARY_TTFT SUMMARY_STATUS SUMMARY_DEGRADATION

for MODEL in $MODELS; do
    IDX=$((IDX + 1))
    log "${BOLD}============================================================${NC}"
    log "${BOLD}  [$IDX/$MODEL_COUNT] $MODEL${NC}"
    log "${BOLD}============================================================${NC}"
    log ""

    # Unload previous model to get a clean measurement
    unload_all

    # Load this model
    if ! warmup_model "$MODEL"; then
        FAIL=$((FAIL + 1))
        SUMMARY_STATUS[$MODEL]="FAIL"
        log ""
        continue
    fi

    # Benchmark it
    if run_benchy "$MODEL"; then
        PASS=$((PASS + 1))
        SUMMARY_STATUS[$MODEL]="OK"

        # Extract numbers for final summary from JSON
        local_json="$RESULTS_DIR/${MODEL//\//_}_${TIMESTAMP}.json"
        if [[ -f "$local_json" ]]; then
            eval "$(python3 <<PYEOF
import json
with open('$local_json') as f:
    data = json.load(f)

# Collect baseline (depth=0) and deepest results
baseline_tg = None
deepest_tg = None
max_depth = 0

for b in data.get('benchmarks', []):
    depth = b.get('context_size', 0)
    pp  = b.get('pp_throughput') or {}
    tg  = b.get('tg_throughput') or {}
    pk  = b.get('peak_throughput') or {}
    e2e = b.get('e2e_ttft') or {}
    pp_mean  = pp.get('mean', 0) or 0
    pp_std   = pp.get('std', 0) or 0
    tg_mean  = tg.get('mean', 0) or 0
    tg_std   = tg.get('std', 0) or 0
    pk_mean  = pk.get('mean', 0) or 0
    e2e_mean = e2e.get('mean', 0) or 0

    if pp_mean == 0 and tg_mean == 0:
        continue

    if depth == 0:
        baseline_tg = tg_mean
        # Use baseline for summary row
        if pp_std > 0.5:
            print(f"SUMMARY_PP['$MODEL']='{pp_mean:.0f} +/-{pp_std:.0f}'")
        else:
            print(f"SUMMARY_PP['$MODEL']='{pp_mean:.0f}'")
        if tg_std > 0.5:
            print(f"SUMMARY_TG['$MODEL']='{tg_mean:.1f} +/-{tg_std:.1f}'")
        else:
            print(f"SUMMARY_TG['$MODEL']='{tg_mean:.1f}'")
        print(f"SUMMARY_PEAK['$MODEL']='{pk_mean:.0f}'")
        if e2e_mean > 0:
            print(f"SUMMARY_TTFT['$MODEL']='{e2e_mean:.0f}'")

    if depth > max_depth:
        max_depth = depth
        deepest_tg = tg_mean

# Calculate degradation
if baseline_tg and deepest_tg and baseline_tg > 0 and max_depth > 0:
    pct = ((deepest_tg - baseline_tg) / baseline_tg) * 100
    print(f"SUMMARY_DEGRADATION['$MODEL']='{pct:+.1f}% @{max_depth//1024}k'")

PYEOF
            )" 2>/dev/null
        fi
    else
        FAIL=$((FAIL + 1))
        SUMMARY_STATUS[$MODEL]="FAIL"
    fi
    log ""
done

# Final cleanup
unload_all

TOTAL_END=$(date +%s.%N)
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
TOTAL_MIN=$(echo "scale=1; $TOTAL_ELAPSED / 60" | bc)

# =============================================
# FINAL REPORT
# =============================================
log ""
log "${BOLD}============================================================${NC}"
log "${BOLD}  BENCHMARK REPORT — DGX Spark${NC}"
log "${BOLD}============================================================${NC}"
log ""
log "  Date: $(date '+%Y-%m-%d %H:%M')  |  Mode: $MODE  |  Runs: $RUNS"
log "  Depths tested: $DEPTH"
log "  Models tested: $MODEL_COUNT  |  Passed: ${GREEN}$PASS${NC}  Failed: ${RED}$FAIL${NC}"
log "  Total benchmark time: ${TOTAL_MIN} min"
log ""
log "  ${BOLD}$(printf '%-42s  %14s  %12s  %8s  %8s  %14s' 'Model' 'Read (pp)' 'Write (tg)' 'Peak' 'TTFT' 'Deep ctx')${NC}"
log "  ${DIM}$(printf '%-42s  %14s  %12s  %8s  %8s  %14s' '' 'tok/s' 'tok/s' 'tok/s' 'ms' 'degradation')${NC}"
log "  $(printf '%.0s-' {1..106})"

for MODEL in $MODELS; do
    local_name="$MODEL"
    [[ ${#local_name} -gt 42 ]] && local_name="${local_name:0:39}..."

    status="${SUMMARY_STATUS[$MODEL]:-FAIL}"
    pp="${SUMMARY_PP[$MODEL]:-—}"
    tg="${SUMMARY_TG[$MODEL]:-—}"
    peak="${SUMMARY_PEAK[$MODEL]:-—}"
    ttft="${SUMMARY_TTFT[$MODEL]:-—}"
    degrad="${SUMMARY_DEGRADATION[$MODEL]:-—}"

    if [[ "$status" == "OK" ]]; then
        printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "$pp" "$tg" "$peak" "$ttft" "$degrad"
        log "${GREEN}${line}${NC}"
    else
        printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "FAIL" "—" "—" "—" "—"
        log "${RED}${line}${NC}"
    fi
done

log ""
log "  ${DIM}---------------------------------------------------------------${NC}"
log "  ${DIM}How to read this table:${NC}"
log "  ${DIM}${NC}"
log "  ${DIM}  Read (pp)    = How fast the model reads your prompt (higher = better).${NC}"
log "  ${DIM}  Write (tg)   = How fast the model types its answer at depth=0 baseline.${NC}"
log "  ${DIM}                  This is the speed you feel when chatting.${NC}"
log "  ${DIM}                  Humans read at ~4 tok/s, so 20+ feels smooth.${NC}"
log "  ${DIM}  Peak         = Fastest burst speed observed in a 1-second window.${NC}"
log "  ${DIM}  TTFT         = Time until the first word appears (lower = better).${NC}"
log "  ${DIM}  Deep ctx     = Speed change at max tested depth vs baseline.${NC}"
log "  ${DIM}                  >-15% = unified memory bandwidth bottleneck.${NC}"
log "  ${DIM}---------------------------------------------------------------${NC}"
log ""
log "  Results saved to:"
log "    Report     : $REPORT_FILE"
log "    JSON data  : $RESULTS_DIR/*_${TIMESTAMP}.json"
log "    Forum tables: $RESULTS_DIR/*_${TIMESTAMP}.md"
log ""
log "  ${DIM}Tip: To share on NVIDIA forums, copy the llama-benchy tables${NC}"
log "  ${DIM}from the .md files — they use the standard format everyone knows.${NC}"
log "${BOLD}============================================================${NC}"

What test settings make the most sense.. on a single spark?

would be nice to have something like a benchmark tool that sends the results to a database and where you can see if your results are good or bad compared to others with the same hardware.
Similar to “3D Mark” back in the days…

Look at https://spark-arena.com/ :)

@eugr nice tip! I’ve been busy since then. The script grew quite a bit — sharing the updated version.

I see you got your own Model loader sparkrun which looks cool - but its incompatible with my current setup.

I like the auto load feature of lama-swap so it does not matter what I use (Visual Studio Code, Open Web UI, Openclaw…) I just pick the model I like to use in that software and lama-swap unloads the current one and loads the one I selected in the app. Without me having to manually run a command and load the model.

Still I would like to participate in the leaderboard and submit results.

So I told the AI to update the benchmark-models.sh to run the benchmarks as they are required for the Leader board and save the settings and results so they can be submitted.
And to not flood the leader board with slow results check if there is a faster one already and if that is the case skip the submission.

Also I noticed sometimes the model is Fast but just spits out nonsense “!!!” so I added a little Coherence check where I can see a part of the output to check if that model is working or not.

Coherence check: "Here's a classic "Hello, World!" script in Python:```pythonprint("Hello, World!")```Run it by saving

it as `hello.py` and executing in your terminal:`"

everything from here on is AI generated

What’s new in benchmark-models.sh since the last post:


Coherence check before benchmarking

The script now does a warmup request and checks the response before wasting time running llama-benchy. It detects two failure modes:

  • Empty response → FAIL
  • Repetition loop (e.g. model stuck outputting n8n n8n n8n...) → INCOHERENT

Both get skipped cleanly in the summary table — yellow for INCOHERENT, red for FAIL, so you can tell at a glance whether a model crashed or just went degenerate.


More benchmark profiles

./benchmark-models.sh                        # Medium Log (default) — depth 0 + 16384
./benchmark-models.sh --stress               # + depth 32768
./benchmark-models.sh --extreme              # + depth 65535
./benchmark-models.sh --quick Nemotron       # Fast smoke test (pp512, 1 run)
./benchmark-models.sh --runs 5 Qwen3         # Custom run count

--arena mode — spark-arena.com submission files

Since eugr pointed me at spark-arena.com, I added a --arena flag that runs the exact leaderboard benchmark spec and generates the submission files automatically:

./benchmark-models.sh --arena Qwen3-Coder-Next-int4-AutoRound

This runs the official profile:

  • depths: 0 4096 8192 16384 32768 65535 100000
  • concurrency: 1 2 5 10
  • prefix caching enabled

And outputs per model in test-results/arena-submission/<timestamp>/<model>/:

  • results.csv — llama-benchy output in the arena CSV format
  • recipe.yaml — pre-filled spark-arena recipe YAML with correct container, env vars, quantization flags etc.

The script also tracks personal bests in arena-best-results.json. After each run it compares — if it’s a new best, it auto-downloads spark-arena-cli and prints the exact submit command:

★ NEW PERSONAL BEST — 66.3 tg tok/s (was 60.4)
spark-arena-cli benchmark test-results/arena-submission/.../recipe.yaml

Latest numbers on a single DGX Spark (GB10 Blackwell)

All vLLM unless noted. Depth=0, concurrency=1, 3 runs.

Model tg tok/s pp tok/s TTFT Deep ctx (-@16k)
Qwen3-Coder-Next-int4-AutoRound 66.3 3,268 644ms -3.6%
Qwen3.5-35B Uncensored Q4_K_M (llama.cpp) 56.8 1,974 1160ms
Qwen3-VL-30B-FP8 52.5 5,827 508ms -12.2%
Nemotron-3-Nano-30B NVFP4 44.0 3,529 290ms
Nemotron-3-Nano-4B FP8 40.0 9,677 340ms
GPT-OSS-120B MXFP4 33.1 4,624 595ms -8.4%
Qwen3-Coder-Next-FP8-Dynamic 32.6 1,835 1278ms -0.4%
Qwen3-Omni-30B 30.6 2,807 584ms -15.0% ⚠️
Qwen3.5-122B int4-AutoRound 25.6 1,760 1210ms -2.3%
Nemotron-3-Super-120B NVFP4 14.8 1,725 1221ms -1.6%

Qwen3-Coder-Next-int4-AutoRound is the clear speed king at 66 tg tok/s — nearly double the FP8 variant of the same model (32 tok/s). The int4-AutoRound quantization on the 480B MoE actually outperforms everything else in generation speed, which surprised me.

The 30B Omni model hit the -15% threshold at depth 16k — that’s the unified memory bandwidth bottleneck showing up.

It happens that the DGX Spark crashes or reboots during a test so there is a resume function for that.

benchmark-models.sh --resume:

  • Every session writes a checkpoint to test-results/checkpoints/session_TIMESTAMP.json
  • Each model is written to the checkpoint before warmup starts, so a crash mid-model is visible
  • --resume reads the last session, skips completed models, and tells you which model was in-progress when it crashed
  • The final report always shows the checkpoint path and a resume hint if any models failed

Script is below and on GitHub (in my stack repo):

benchmark-models.sh

#!/bin/bash

=============================================================================

benchmark-models.sh — Benchmark all llama-swap models using llama-benchy

Uses llama-benchy (GitHub - eugr/llama-benchy: llama-benchy - llama-bench style benchmarking tool for all backends · GitHub) for standardized

LLM performance measurement. Results are comparable with other DGX Spark

users who use the same tool.

Benchmark profiles simulate real-world document analysis workloads:

“Medium Log” (default) — 50-page document, pp2048 + tg128 @ depth 16384

“Massive Log” (–stress) — 100+ page log, pp2048 + tg128 @ depth 32768

“Extreme” (–extreme)— 200+ page corpus, pp2048 + tg128 @ depth 65535

Each profile includes a depth=0 baseline so you can see the performance

delta as unified memory pressure increases.

Usage:

./benchmark-models.sh # Medium Log (default)

./benchmark-models.sh --stress # Medium + Massive Log

./benchmark-models.sh --extreme # All three depth levels

./benchmark-models.sh --quick Nemotron # Fast smoke test

./benchmark-models.sh --runs 5 Qwen3.5-35B # Custom run count

./benchmark-models.sh Qwen3.5 Nemotron # Only matching models

=============================================================================

set -euo pipefail

LLAMA_SWAP_URL=“${LLAMA_SWAP_URL:-http://localhost:28080}”
SCRIPT_DIR=“$(dirname “$(readlink -f “$0”)”)”
RESULTS_DIR=“$SCRIPT_DIR/test-results/benchmarks”
ARENA_BEST_FILE=“$SCRIPT_DIR/test-results/arena-best-results.json”
TIMEOUT=1800

spark-arena-cli: installed to ~/.local/bin on first --arena run

SPARK_CLI=“${HOME}/.local/bin/spark-arena-cli”

Colors

GREEN=‘\033[0;32m’
RED=‘\033[0;31m’
YELLOW=‘\033[1;33m’
CYAN=‘\033[0;36m’
BOLD=‘\033[1m’
DIM=‘\033[2m’
NC=‘\033[0m’

Defaults: “Medium Log” baseline

PP=“2048”
TG=“128”
DEPTH=“0 16384”
RUNS=3
MODE=“medium-log”
CONCURRENCY=“” # space-separated list; empty = not passed to llama-benchy
ARENA_DIR=“” # set by --arena mode
FILTERS=()

Crash-resume tracking

CHECKPOINT_DIR=“$SCRIPT_DIR/test-results/checkpoints”
LAST_SESSION_FILE=“$SCRIPT_DIR/test-results/.last-session”
CHECKPOINT_FILE=“”
RESUME=false
SKIP_MODELS=()

Parse arguments

while [[ $# -gt 0 ]]; do
case “$1” in
–quick)
PP=“512”
TG=“128”
DEPTH=“0”
RUNS=1
MODE=“quick”
shift
;;
–stress)
# Medium Log + Massive Log
PP=“2048”
TG=“128”
DEPTH=“0 16384 32768”
RUNS=3
MODE=“stress”
shift
;;
–extreme)
# All three: Medium + Massive + Extreme limit
PP=“2048”
TG=“128”
DEPTH=“0 16384 32768 65535”
RUNS=3
MODE=“extreme”
shift
;;
–full)
# Comprehensive sweep (original broad test)
PP=“512 2048”
TG=“128 256 512”
DEPTH=“0 16384 32768”
RUNS=3
MODE=“full”
shift
;;
–arena)
# Spark-Arena leaderboard submission profile
# Spark Arena - LLM Leaderboard
PP=“2048”
TG=“128”
DEPTH=“0 4096 8192 16384 32768 65535 100000”
CONCURRENCY=“1 2 5 10”
RUNS=3
MODE=“arena”
shift
;;
–runs)
RUNS=“$2”
shift 2
;;
–runs=)
RUNS="${1#
=}"
shift
;;
–resume)
RESUME=true
shift
;;
–help|-h)
cat <<‘HELPEOF’
Usage: benchmark-models.sh [OPTIONS] [FILTER…]

Benchmark your llama-swap models using llama-benchy.
Results use the standard llama-benchy format for comparison
with other DGX Spark users on the NVIDIA forums.

Profiles (simulating real-world document workloads):

(default) “Medium Log” — pp2048, tg128 @ depth 0 + 16384
Simulates a ~50-page document. The depth=0 baseline shows
raw speed; depth=16384 shows the cost of a full KV cache.

–stress “Massive Log” — adds depth 32768
Doubles the context to simulate a massive error log.
Watch for tg tok/s drop vs. the medium baseline — that’s
where shared memory bandwidth starts to bottleneck.

–extreme “Extreme Limit” — adds depth 65535
Pushes to ~100 pages of text. Strictly to see if the
system can process it without crashing or heavy swap.

–quick Smoke test — pp512, tg128, depth 0, 1 run
–full Broad sweep — pp512+2048, tg128+256+512, depths 0-32k
–arena Spark-Arena leaderboard profile — exact spec from Spark Arena - LLM Leaderboard
Saves results.csv + recipe.yaml per model to test-results/arena-submission/
Depths: 0 4096 8192 16384 32768 65535 100000 | Concurrency: 1 2 5 10

Other options:
–runs N Override number of runs (default: 3)
–resume Resume from the last interrupted session (skip already-completed models)
–help Show this help

Filters:
Add model name fragments to only test matching models.
Example: ./benchmark-models.sh --stress Qwen3.5 Nemotron

Environment:
LLAMA_SWAP_URL llama-swap endpoint (default: http://localhost:28080)

What the numbers mean:
pp tok/s = Prompt Processing speed. How fast the model reads your input.
Higher is better. Typically 500-5000+ tok/s on DGX Spark.
tg tok/s = Token Generation speed. How fast the model writes its reply.
Higher is better. This is the number you “feel” when chatting.
Typically 15-50+ tok/s on DGX Spark depending on model size.
TTFT = Time To First Token. The delay before the model starts replying.
Lower is better. Measured in milliseconds.

Key insight:
Compare tg tok/s across depths. A big drop from depth=16384 to depth=32768
means you’ve found the unified memory bandwidth bottleneck between the ARM
CPU and the Blackwell GPU on DGX Spark.
HELPEOF
exit 0
;;
*)
FILTERS+=(“$1”)
shift
;;
esac
done

mkdir -p “$RESULTS_DIR”
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_FILE=“$RESULTS_DIR/report_${TIMESTAMP}.txt”

if [[ “$MODE” == “arena” ]]; then
ARENA_DIR=“$SCRIPT_DIR/test-results/arena-submission/${TIMESTAMP}”
mkdir -p “$ARENA_DIR”
fi

log() { echo -e “$1” | tee -a “$REPORT_FILE”; }

unload_all() {
curl -sf -X POST “$LLAMA_SWAP_URL/unload” > /dev/null 2>&1 || true
sleep 5
}

Warm up: send a tiny request to make llama-swap load the model

warmup_model() {
local model=“$1”
log " Loading model via llama-swap…"
local start end elapsed
start=$(date +%s.%N)

local response_file="/tmp/response_${TIMESTAMP}.json"
local http_code
http_code=$(curl -s -w "%{http_code}" -o "$response_file" --max-time "$TIMEOUT" \
    -X POST "$LLAMA_SWAP_URL/v1/chat/completions" \
    -H "Content-Type: application/json" \
    -d "$(jq -n --arg model "$model" '{
        model: $model,
        messages: [{role: "user", content: "Write a short python hello world script."}],
        max_tokens: 50
    }')" 2>/dev/null) || http_code=0

end=$(date +%s.%N)
elapsed=$(echo "scale=1; $end - $start" | bc)

if [[ "$http_code" -ne 200 ]]; then
    local err_msg
    err_msg=$(jq -r '.error.message // empty' "$response_file" 2>/dev/null)
    if [[ -n "$err_msg" ]]; then
        log "  ${RED}FAILED to load: $err_msg (HTTP $http_code)${NC}"
    else
        log "  ${RED}FAILED to load (HTTP $http_code)${NC}"
    fi
    rm -f "$response_file"
    return 1
fi

local content
content=$(jq -r '(.choices[0].message.reasoning_content // "") + (.choices[0].message.reasoning // "") + (.choices[0].message.content // "")' "$response_file" 2>/dev/null | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
rm -f "$response_file"

if [[ -z "$content" ]]; then
    log "  ${CYAN}Coherence check:${NC} ${RED}FAILED — empty response${NC}"
    WARMUP_FAIL_REASON="EMPTY"
    return 1
fi

# Detect repetition loop: split into words, check if any single word
# makes up >60% of total words (e.g. "n8n n8n n8n..." or "the the the...")
local word_count most_freq_count most_freq_word
word_count=$(echo "$content" | wc -w)
if [[ "$word_count" -ge 5 ]]; then
    most_freq_word=$(echo "$content" | tr ' ' '\n' | sort | uniq -c | sort -rn | awk 'NR==1{print $2}')
    most_freq_count=$(echo "$content" | tr ' ' '\n' | grep -cFx "$most_freq_word" 2>/dev/null || echo 0)
    local pct=$(( most_freq_count * 100 / word_count ))
    if [[ "$pct" -ge 60 ]]; then
        log "  ${CYAN}Coherence check:${NC} ${RED}FAILED — repetition loop (\"${most_freq_word}\" = ${pct}% of output)${NC}"
        log "  ${RED}Skipping benchmark — model output is incoherent.${NC}"
        WARMUP_FAIL_REASON="INCOHERENT"
        return 1
    fi
fi

log "  ${CYAN}Coherence check:${NC} \"${content:0:150}\""
log "  Model ready (loaded in ${elapsed}s)"
WARMUP_FAIL_REASON=""
return 0

}

Run llama-benchy and capture results

run_benchy() {
local model=“$1”
local safe_name=“${model////}"
local json_file="$RESULTS_DIR/${safe_name}
${TIMESTAMP}.json”
local md_file=“$RESULTS_DIR/${safe_name}_${TIMESTAMP}.md”

# Show profile description
case "$MODE" in
    medium-log) log "  Profile: Medium Log (50-page document baseline)" ;;
    stress)     log "  Profile: Massive Log (stress test — watch for bandwidth bottleneck)" ;;
    extreme)    log "  Profile: Extreme Limit (push to ~100 pages, crash/swap detection)" ;;
    quick)      log "  Profile: Quick smoke test" ;;
    full)       log "  Profile: Full comprehensive sweep" ;;
    arena)      log "  Profile: Spark-Arena leaderboard (7 depths × 4 concurrency levels)" ;;
esac
local concurrency_display=""
[[ -n "$CONCURRENCY" ]] && concurrency_display="  concurrency=$CONCURRENCY"
log "  Running llama-benchy (pp=$PP  tg=$TG  depth=$DEPTH  runs=$RUNS${concurrency_display})..."
log ""

# Build shared base flags (used by all runs)
local base_flags=""
base_flags+=" --base-url $LLAMA_SWAP_URL/v1"
base_flags+=" --model $model"
base_flags+=" --pp $PP"
base_flags+=" --tg $TG"
base_flags+=" --depth $DEPTH"
base_flags+=" --runs $RUNS"
base_flags+=" --latency-mode generation"
base_flags+=" --no-warmup"
base_flags+=" --skip-coherence"
[[ -n "$CONCURRENCY" ]] && base_flags+=" --concurrency $CONCURRENCY"
[[ "$MODE" == "arena" ]] && base_flags+=" --enable-prefix-caching"

# --- Run 1: Save JSON for data parsing ---
local cmd_json="uvx llama-benchy${base_flags} --save-result ${json_file} --format json"

local output exit_code=0
output=$(eval "$cmd_json" 2>&1) || exit_code=$?

if [[ $exit_code -ne 0 ]]; then
    log "  ${RED}llama-benchy failed:${NC}"
    echo "$output" | tail -15 | tee -a "$REPORT_FILE"
    return 1
fi

# --- Arena mode: save submission CSV (separate run, same params) ---
if [[ "$MODE" == "arena" && -n "$ARENA_DIR" ]]; then
    local arena_model_dir="$ARENA_DIR/${safe_name}"
    mkdir -p "$arena_model_dir"
    local csv_file="$arena_model_dir/results.csv"
    local cmd_csv="uvx llama-benchy${base_flags} --save-result ${csv_file} --format csv"
    log "  ${CYAN}Saving arena submission CSV...${NC}"
    local csv_output csv_exit=0
    csv_output=$(eval "$cmd_csv" 2>&1) || csv_exit=$?
    if [[ $csv_exit -ne 0 ]]; then
        log "  ${YELLOW}Warning: CSV run failed — JSON data still saved${NC}"
    else
        log "  ${DIM}Arena CSV  : $csv_file${NC}"
    fi
fi

# --- Run 2: Get the markdown table (for sharing on forums) ---
local cmd_md="uvx llama-benchy${base_flags} --save-result ${md_file} --format md"

local md_output
md_output=$(eval "$cmd_md" 2>&1) || true

# Show the full saved markdown file (the format people share on forums)
if [[ -f "$md_file" ]]; then
    log "  ${CYAN}llama-benchy results (copy this to share on forums):${NC}"
    log ""
    cat "$md_file" | tee -a "$REPORT_FILE"
    log ""
else
    # Fallback: show table lines from stdout if file wasn't created
    local table_lines
    table_lines=$(echo "$md_output" | grep -E '^\|')
    if [[ -n "$table_lines" ]]; then
        log "  ${CYAN}llama-benchy results:${NC}"
        log ""
        echo "$table_lines" | tee -a "$REPORT_FILE"
        log ""
    fi
fi

# --- Parse JSON and show friendly explanation ---
if [[ -f "$json_file" ]]; then
    python3 <<PYEOF | tee -a "$REPORT_FILE"

import json

with open(‘$json_file’) as f:
data = json.load(f)

benchmarks = data.get(‘benchmarks’, )
if not benchmarks:
print(" (no benchmark data found)")
exit(0)

Check if all benchmarks have null results (model failed silently)

all_null = all(
b.get(‘pp_throughput’) is None and b.get(‘tg_throughput’) is None
for b in benchmarks
)
if all_null:
print(" (model returned no usable results — it may not support this benchmark)")
exit(0)

Group benchmarks by depth for comparison

depth_results = {}
for b in benchmarks:
depth = b.get(‘context_size’, 0)
pp_obj = b.get(‘pp_throughput’) or {}
tg_obj = b.get(‘tg_throughput’) or {}
pk_obj = b.get(‘peak_throughput’) or {}
e2e_obj = b.get(‘e2e_ttft’) or {}
pp_mean = pp_obj.get(‘mean’, 0) or 0
tg_mean = tg_obj.get(‘mean’, 0) or 0
if pp_mean > 0 or tg_mean > 0:
depth_results[depth] = {
‘pp_mean’: pp_mean, ‘pp_std’: (pp_obj.get(‘std’, 0) or 0),
‘tg_mean’: tg_mean, ‘tg_std’: (tg_obj.get(‘std’, 0) or 0),
‘pk_mean’: (pk_obj.get(‘mean’, 0) or 0),
‘e2e_mean’: (e2e_obj.get(‘mean’, 0) or 0),
}

Depth label mapping

depth_labels = {
0: “Baseline (no context)”,
16384: “Medium Log (~50 pages)”,
32768: “Massive Log (~100 pages)”,
65535: “Extreme Limit (~200 pages)”,
}

print(" Context Depth Analysis:“)
print(”")

baseline_tg = None
for depth in sorted(depth_results.keys()):
r = depth_results[depth]
label = depth_labels.get(depth, f"depth {depth}")
pp_mean, pp_std = r[‘pp_mean’], r[‘pp_std’]
tg_mean, tg_std = r[‘tg_mean’], r[‘tg_std’]
pk_mean = r[‘pk_mean’]
e2e_mean = r[‘e2e_mean’]

print(f"  --- depth={depth:,} — {label} ---")

# Reading speed
if pp_mean > 0:
    if pp_std > 0.5:
        print(f"    Reading speed:  {pp_mean:,.0f} +/- {pp_std:,.0f} tok/s")
    else:
        print(f"    Reading speed:  {pp_mean:,.0f} tok/s")

# Writing speed
if tg_mean > 0:
    if tg_std > 0.5:
        print(f"    Writing speed:  {tg_mean:.1f} +/- {tg_std:.1f} tok/s  (peak: {pk_mean:.0f})")
    else:
        print(f"    Writing speed:  {tg_mean:.1f} tok/s  (peak: {pk_mean:.0f})")

    # Show degradation from baseline
    if baseline_tg is None:
        baseline_tg = tg_mean
    elif baseline_tg > 0:
        pct = ((tg_mean - baseline_tg) / baseline_tg) * 100
        if pct < -15:
            print(f"    {chr(9888)}  {pct:+.1f}% vs baseline — BANDWIDTH BOTTLENECK DETECTED")
        elif pct < -5:
            print(f"      -> {pct:+.1f}% vs baseline (moderate slowdown)")
        else:
            print(f"      -> {pct:+.1f}% vs baseline (minimal impact)")

    # What it feels like
    if tg_mean >= 40:
        feel = "Very fast — feels instant, smooth streaming"
    elif tg_mean >= 25:
        feel = "Fast — comfortable for interactive chat"
    elif tg_mean >= 15:
        feel = "Good — readable streaming with slight pauses"
    elif tg_mean >= 8:
        feel = "Moderate — noticeable wait, but usable"
    else:
        feel = "Slow — may feel sluggish for chat"
    print(f"      -> {feel}")

# TTFT
if e2e_mean > 0:
    print(f"    Time to first token:  {e2e_mean:.0f}ms")
    if e2e_mean < 200:
        print(f"      -> Feels instant")
    elif e2e_mean < 500:
        print(f"      -> Barely noticeable delay")
    elif e2e_mean < 2000:
        print(f"      -> Short pause before response starts")
    else:
        print(f"      -> Noticeable wait ({e2e_mean/1000:.1f}s)")
print("")

Summary comparison if multiple depths

if len(depth_results) > 1 and baseline_tg and baseline_tg > 0:
max_depth = max(depth_results.keys())
deepest_tg = depth_results[max_depth][‘tg_mean’]
total_pct = ((deepest_tg - baseline_tg) / baseline_tg) * 100
print(f" Overall impact: depth 0 → {max_depth:,} = {total_pct:+.1f}% generation speed")
if total_pct < -20:
print(f" Conclusion: Significant unified memory bandwidth bottleneck at depth {max_depth:,}“)
elif total_pct < -10:
print(f” Conclusion: Moderate bandwidth pressure — usable but noticeably slower")
else:
print(f" Conclusion: Model handles deep context well on this hardware")
print(“”)

PYEOF
fi

log "  ${DIM}JSON data : $json_file${NC}"
log "  ${DIM}Forum table: $md_file${NC}"
return 0

}

---------------------------------------------------------------------------

spark-arena-cli helpers

---------------------------------------------------------------------------

Download spark-arena-cli binary if it isn’t already on PATH / ~/.local/bin

install_spark_arena_cli() {
if command -v spark-arena-cli &>/dev/null; then
SPARK_CLI=“$(command -v spark-arena-cli)”
return 0
fi
if [[ -x “$SPARK_CLI” ]]; then
return 0
fi
local arch
arch=$(uname -m)
local bin_name
[[ “$arch” == “aarch64” || “$arch” == “arm64” ]]
&& bin_name=“spark-arena-cli-0.1.0-linux-arm64”
|| bin_name=“spark-arena-cli-0.1.0-linux-amd64”
local url=“https://github.com/spark-arena/spark-arena-cli/releases/download/v0.1.0/${bin_name}
log " ${DIM}Downloading spark-arena-cli from GitHub releases…${NC}"
mkdir -p “$(dirname “$SPARK_CLI”)”
if curl -fsSL “$url” -o “$SPARK_CLI” 2>/dev/null; then
chmod +x “$SPARK_CLI”
log " ${DIM}Installed to $SPARK_CLI${NC}"
# Add to PATH for this session
export PATH=“$(dirname “$SPARK_CLI”):$PATH”
else
log " ${YELLOW}Warning: could not download spark-arena-cli — manual install needed${NC}"
SPARK_CLI=“”
fi
}

Read our personal best tg tok/s (depth=0, concurrency=1) for a model from the history file

get_personal_best_tg() {
local model=“$1”
if [[ ! -f “$ARENA_BEST_FILE” ]]; then echo “0”; return; fi
python3 -c "
import json, sys
try:
d = json.load(open(‘$ARENA_BEST_FILE’))
entry = d.get(‘$model’, {})
print(entry.get(‘tg_mean’, 0))
except:
print(0)
" 2>/dev/null || echo “0”
}

Save current result as personal best for a model

save_personal_best() {
local model=“$1”
local json_file=“$2”
python3 - “$model” “$json_file” “$ARENA_BEST_FILE” <<‘PYEOF’
import json, sys, os
model, bench_json, best_file = sys.argv[1], sys.argv[2], sys.argv[3]

try:
data = json.load(open(bench_json))
except Exception as e:
sys.exit(0)

Find depth=0, concurrency=1 entry

baseline = None
for b in data.get(“benchmarks”, ):
if b.get(“context_size”) == 0 and b.get(“concurrency”) == 1:
baseline = b
break
if not baseline:
# Fallback: first entry with context_size=0
for b in data.get(“benchmarks”, ):
if b.get(“context_size”) == 0:
baseline = b
break
if not baseline:
sys.exit(0)

tg = (baseline.get(“tg_throughput”) or {}).get(“mean”, 0) or 0
pp = (baseline.get(“pp_throughput”) or {}).get(“mean”, 0) or 0
e2e = (baseline.get(“e2e_ttft”) or {}).get(“mean”, 0) or 0

try:
best = json.load(open(best_file)) if os.path.exists(best_file) else {}
except:
best = {}

best[model] = {
“tg_mean”: round(tg, 2),
“pp_mean”: round(pp, 1),
“ttft_ms”: round(e2e, 1),
“timestamp”: data.get(“timestamp”, “”),
“depth”: 0,
“concurrency”: 1,
}
with open(best_file, “w”) as f:
json.dump(best, f, indent=2)
PYEOF
}

Compare current result vs personal best; if better, print submission info

check_and_suggest_submit() {
local model=“$1”
local json_file=“$2”
local recipe_file=“$3”

local prev_best
prev_best=$(get_personal_best_tg "$model")

local current_tg
current_tg=$(python3 -c "

import json
data = json.load(open(‘$json_file’))
for b in data.get(‘benchmarks’, ):
if b.get(‘context_size’) == 0 and b.get(‘concurrency’) == 1:
tg = (b.get(‘tg_throughput’) or {}).get(‘mean’, 0) or 0
print(round(tg, 2))
exit()
for b in data.get(‘benchmarks’, ):
if b.get(‘context_size’) == 0:
tg = (b.get(‘tg_throughput’) or {}).get(‘mean’, 0) or 0
print(round(tg, 2))
exit()
print(0)
" 2>/dev/null || echo “0”)

local is_better=0
python3 -c "exit(0 if float('$current_tg') > float('$prev_best') else 1)" 2>/dev/null && is_better=1

if [[ "$is_better" -eq 1 ]]; then
    if python3 -c "exit(0 if float('$prev_best') > 0 else 1)" 2>/dev/null; then
        log "  ${GREEN}★ NEW PERSONAL BEST${NC} — ${current_tg} tg tok/s (was ${prev_best})"
    else
        log "  ${GREEN}★ FIRST ARENA RESULT${NC} — ${current_tg} tg tok/s @ depth=0 concurrency=1"
    fi
    save_personal_best "$model" "$json_file"

    # Try to get leaderboard context (best-effort scrape — may be empty)
    log "  ${CYAN}Submission candidate${NC} — consider submitting to spark-arena.com/leaderboard"
    log ""

    install_spark_arena_cli

    if [[ -n "$SPARK_CLI" && -x "$SPARK_CLI" ]]; then
        local is_logged_in=0
        "$SPARK_CLI" benchmark --help &>/dev/null && {
            # Probe login state: spark-arena-cli prints a warning if not configured
            local probe
            probe=$(echo "exit" | timeout 3 "$SPARK_CLI" 2>&1 || true)
            echo "$probe" | grep -q "Warning: Configuration not found" || is_logged_in=1
        }

        if [[ "$is_logged_in" -eq 1 ]]; then
            log "  ${GREEN}spark-arena-cli is logged in.${NC} Run this to submit officially:"
            log "  ${BOLD}  $SPARK_CLI benchmark $recipe_file${NC}"
            log "  ${DIM}  (This re-runs the benchmark via sparkrun and auto-uploads results)${NC}"
        else
            log "  ${YELLOW}spark-arena-cli installed but not logged in.${NC} To submit:"
            log "  ${DIM}  1. $SPARK_CLI login         # authenticate via Google/GitHub${NC}"
            log "  ${DIM}  2. $SPARK_CLI setup         # configure sparkrun + llama-benchy${NC}"
            log "  ${DIM}  3. $SPARK_CLI benchmark $recipe_file${NC}"
        fi
    else
        log "  ${DIM}To submit to spark-arena, install spark-arena-cli:${NC}"
        log "  ${DIM}  curl -fsSL https://github.com/spark-arena/spark-arena-cli/releases/download/v0.1.0/spark-arena-cli-0.1.0-linux-amd64 -o ~/.local/bin/spark-arena-cli && chmod +x ~/.local/bin/spark-arena-cli${NC}"
        log "  ${DIM}  spark-arena-cli login${NC}"
        log "  ${DIM}  spark-arena-cli benchmark $recipe_file${NC}"
    fi
    log ""
else
    log "  ${DIM}tg ${current_tg} tok/s — personal best is ${prev_best} tok/s (no improvement, skipping submission)${NC}"
fi

}

Generate a spark-arena recipe.yaml for a model

generate_recipe_yaml() {
local model=“$1”
local safe_name=“${model////_}”
local out_dir=“$ARENA_DIR/${safe_name}”
mkdir -p “$out_dir”

python3 - "$model" "$out_dir/recipe.yaml" <<'PYEOF'

import sys, textwrap

model_name = sys.argv[1]
out_path = sys.argv[2]

Per-model recipe metadata.

container: the local Docker image tag we actually use.

hf_model: canonical HuggingFace model ID for the submission.

RECIPES = {
“Qwen3.5-35B-A3B-FP8”: {
“hf_model”: “Qwen/Qwen3.5-35B-A3B-Instruct”,
“description”: “Qwen3.5 35B MoE FP8-dynamic — reasoning + tool use with MTP-2 speculation”,
“container”: “vllm-node:Version_1”,
“tp”: 1, “gpu_mem”: 0.7, “max_len”: 131072,
“env”: {
“VLLM_MARLIN_USE_ATOMIC_ADD”: “1”,
“VLLM_ENABLE_CUDAGRAPH_GC”: “1”,
“VLLM_USE_FLASHINFER_SAMPLER”: “1”,
},
“extras”: [
“–kv-cache-dtype fp8”,
“–load-format fastsafetensors”,
“–attention-backend FLASHINFER”,
“–enable-prefix-caching”,
“–enable-chunked-prefill”,
“–max-num-batched-tokens 4096”,
‘–speculative-config '{“method”:“mtp”,“num_speculative_tokens”:2}'’,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_xml”,
“–reasoning-parser qwen3”,
],
},
“Qwen3.5-122B-A10B-int4-AutoRound”: {
“hf_model”: “Qwen/Qwen3.5-122B-A10B-Instruct”,
“description”: “Qwen3.5 122B MoE INT4 AutoRound — large hybrid reasoning model”,
“container”: “vllm-node-tf5:latest”,
“tp”: 1, “gpu_mem”: 0.75, “max_len”: 40960,
“env”: {“VLLM_MARLIN_USE_ATOMIC_ADD”: “1”},
“extras”: [
“–trust-remote-code”,
“–enforce-eager”,
“–kv-cache-dtype fp8”,
“–enable-prefix-caching”,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_xml”,
“–reasoning-parser qwen3”,
],
},
“Qwen3-VL-30B-A3B-Instruct-FP8”: {
“hf_model”: “Qwen/Qwen3-VL-30B-A3B-Instruct”,
“description”: “Qwen3-VL 30B MoE FP8 — vision-language model”,
“container”: “spark-vllm:Version_1”,
“tp”: 1, “gpu_mem”: 0.60, “max_len”: 32768,
“env”: {},
“extras”: [
“–trust-remote-code”,
“–kv-cache-dtype fp8”,
“–load-format fastsafetensors”,
“–enable-prefix-caching”,
“–limit-mm-per-prompt ‘{"image": 2}’”,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_coder”,
],
},
“Qwen3-Omni-30B-A3B-Instruct”: {
“hf_model”: “Qwen/Qwen3-Omni-30B-A3B-Instruct”,
“description”: “Qwen3-Omni 30B MoE — audio + vision + text multimodal”,
“container”: “vllm-node:Version_1”,
“tp”: 1, “gpu_mem”: 0.75, “max_len”: 32768,
“env”: {},
“extras”: [
“–trust-remote-code”,
“–load-format fastsafetensors”,
“–enable-prefix-caching”,
“–limit-mm-per-prompt ‘{"image": 2, "audio": 2}’”,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_coder”,
],
},
“Qwen3-Coder-Next-FP8-Dynamic”: {
“hf_model”: “Qwen/Qwen3-Coder-480B-A22B-FP8-Dynamic”,
“description”: “Qwen3-Coder-Next 480B MoE FP8-Dynamic — coding specialist”,
“container”: “vllm-node-tf5:latest”,
“tp”: 1, “gpu_mem”: 0.75, “max_len”: 32768,
“env”: {},
“extras”: [
“–kv-cache-dtype fp8”,
“–load-format fastsafetensors”,
“–attention-backend flashinfer”,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_coder”,
],
},
“Qwen3-Coder-Next-int4-AutoRound”: {
“hf_model”: “Qwen/Qwen3-Coder-480B-A22B-Instruct”,
“description”: “Qwen3-Coder-Next 480B MoE INT4 AutoRound — coding + throughput optimized”,
“container”: “vllm-node-tf5:latest”,
“tp”: 1, “gpu_mem”: 0.6, “max_len”: 32768,
“env”: {
“VLLM_MARLIN_USE_ATOMIC_ADD”: “1”,
“VLLM_ALLOW_LONG_MAX_MODEL_LEN”: “1”,
“VLLM_USE_FLASHINFER_MOE_FP8”: “1”,
},
“extras”: [
“–language-model-only”,
“–enable-chunked-prefill”,
“–max-num-batched-tokens 49152”,
“–max-num-seqs 384”,
“–kv-cache-dtype fp8”,
“–load-format fastsafetensors”,
“–optimization-level 3”,
“–performance-mode throughput”,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_coder”,
],
},
“Nemotron-3-Nano-4B-FP8”: {
“hf_model”: “nvidia/Nemotron-3-Nano-4B-Instruct”,
“description”: “Nemotron-3-Nano 4B FP8 — ultra-fast orchestrator / routing model”,
“container”: “spark-vllm:Version_1”,
“tp”: 1, “gpu_mem”: 0.5, “max_len”: 8192,
“env”: {},
“extras”: [
“–kv-cache-dtype fp8”,
“–enforce-eager”,
“–trust-remote-code”,
“–load-format fastsafetensors”,
“–enable-prefix-caching”,
“–tool-call-parser qwen3_coder”,
“–reasoning-parser nemotron_v3”,
“–enable-auto-tool-choice”,
],
},
“Nemotron-3-Nano-30B-A3B-NVFP4”: {
“hf_model”: “nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4”,
“description”: “Nemotron-3-Nano 30B NVFP4 — Blackwell-native MoE with nano_v3 reasoning”,
“container”: “vllm-node:Version_1”,
“tp”: 1, “gpu_mem”: 0.65, “max_len”: 131072,
“env”: {
“VLLM_USE_FLASHINFER_MOE_FP4”: “1”,
“VLLM_FLASHINFER_MOE_BACKEND”: “throughput”,
},
“extras”: [
“–kv-cache-dtype fp8”,
“–enforce-eager”,
“–trust-remote-code”,
“–quantization modelopt_fp4”,
“–enable-auto-tool-choice”,
“–tool-call-parser qwen3_coder”,
“–reasoning-parser nano_v3”,
],
},
“Nemotron-3-Super-120B-A12B-NVFP4”: {
“hf_model”: “nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4”,
“description”: “Nemotron-3-Super 120B NVFP4 — large reasoning model with CUTLASS MoE”,
“container”: “spark-vllm:Version_1”,
“tp”: 1, “gpu_mem”: 0.7, “max_len”: 65536,
“env”: {
“VLLM_FLASHINFER_ALLREDUCE_BACKEND”: “trtllm”,
“VLLM_ALLOW_LONG_MAX_MODEL_LEN”: “1”,
},
“extras”: [
“–kv-cache-dtype fp8”,
“–moe-backend cutlass”,
“–trust-remote-code”,
“–enable-prefix-caching”,
“–load-format fastsafetensors”,
“–tool-call-parser qwen3_coder”,
“–enable-auto-tool-choice”,
“–reasoning-parser nemotron_v3”,
],
},
“GPT-OSS-120B”: {
“hf_model”: “openai/gpt-oss-120b”,
“description”: “OpenAI GPT-OSS 120B MXFP4 — open-weights GPT model”,
“container”: “vllm-node-mxfp4”,
“tp”: 1, “gpu_mem”: 0.7, “max_len”: 65536,
“env”: {“VLLM_USE_FLASHINFER_MOE_MXFP4_MXFP8”: “1”},
“extras”: [
“–quantization mxfp4”,
“–kv-cache-dtype fp8”,
“–max-num-batched-tokens 8192”,
“–enable-prefix-caching”,
“–load-format fastsafetensors”,
“–tool-call-parser openai”,
“–reasoning-parser openai_gptoss”,
“–enable-auto-tool-choice”,
],
},
“Mistral-Small-24B-Instruct-2501”: {
“hf_model”: “mistralai/Mistral-Small-24B-Instruct-2501”,
“description”: “Mistral Small 24B — fast roleplay and instruction following”,
“container”: “vllm-node:Version_1”,
“tp”: 1, “gpu_mem”: 0.7, “max_len”: 32768,
“env”: {},
“extras”: [
“–trust-remote-code”,
“–enforce-eager”,
“–enable-auto-tool-choice”,
“–tool-call-parser mistral”,
],
},
“Qwen3.5-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M-GGUF”: {
“hf_model”: “HauhauCS/Qwen3.5-35B-A3B-Uncensored-Aggressive-GGUF”,
“description”: “Qwen3.5 35B MoE Q4_K_M GGUF — uncensored, llama.cpp serving”,
“container”: “ghcr.io/martin-b78/llama-cpp-spark:latest”,
“tp”: 1, “gpu_mem”: 0.0, “max_len”: 16384,
“env”: {“GGML_CUDA_ENABLE_UNIFIED_MEMORY”: “1”},
“extras”: [
“–ctx-size 16384”,
“–n-gpu-layers 99”,
“–parallel 4”,
“–no-mmap”,
],
“runtime”: “llama-cpp”,
},
}

r = RECIPES.get(model_name)
if not r:
# Unknown model — generate a minimal placeholder recipe
r = {
“hf_model”: f"<TODO: HuggingFace model ID for {model_name}>",
“description”: model_name,
“container”: “<TODO: container image>”,
“tp”: 1, “gpu_mem”: 0.7, “max_len”: 32768,
“env”: {}, “extras”: ,
}

runtime = r.get(“runtime”, “vllm”)
hf_model = r[“hf_model”]
extras = “\n”.join(f" {e} \" for e in r[“extras”])
env_block = “”
if r[“env”]:
env_lines = “\n”.join(f" {k}: ‘{v}’" for k, v in r[“env”].items())
env_block = f"env:\n{env_lines}\n"

if runtime == “llama-cpp”:
cmd = (
f"llama-server \\n"
f" -hf {hf_model} \\n"
f" --host {{host}} --port {{port}} \\n"
+ “\n”.join(f" {e} \" for e in r[“extras”])
+ “\n”
)
else:
cmd = (
f"vllm serve {hf_model} \\n"
f" --served-model-name {model_name} \\n"
f" --host {{host}} --port {{port}} \\n"
f" --tensor-parallel-size {{tensor_parallel}} \\n"
f" --gpu-memory-utilization {{gpu_memory_utilization}} \\n"
f" --max-model-len {{max_model_len}} \\n"
+ “\n”.join(f" {e} \" for e in r[“extras”])
+ “\n”
)

yaml = f""“recipe_version: ‘1’
name: {model_name}
description: {r[‘description’]}
model: {hf_model}
cluster_only: false
container: {r[‘container’]}
defaults:
port: 8000
host: 0.0.0.0
tensor_parallel: {r[‘tp’]}
gpu_memory_utilization: {r[‘gpu_mem’]}
max_model_len: {r[‘max_len’]}
{env_block}command: |
{cmd.rstrip()}
solo_only: false
“””

with open(out_path, “w”) as f:
f.write(yaml)

print(f" Recipe YAML: {out_path}")
PYEOF

if [[ $? -ne 0 ]]; then
    log "  ${YELLOW}Warning: recipe.yaml generation failed for $model${NC}"
fi

}

---------------------------------------------------------------------------

Crash-resume checkpoint functions

---------------------------------------------------------------------------

Create a new checkpoint file for this session and register it as last-known

init_checkpoint() {
mkdir -p “$CHECKPOINT_DIR”
CHECKPOINT_FILE=“$CHECKPOINT_DIR/session_${TIMESTAMP}.json”
python3 - “$CHECKPOINT_FILE” “$MODE” “$PP” “$TG” “$DEPTH” “$RUNS” <<‘PYEOF’
import json, sys, datetime
f, mode, pp, tg, depth, runs = sys.argv[1:]
data = {
“session_id”: f.split(“_”)[-1].replace(“.json”, “”),
“started_at”: datetime.datetime.now().isoformat(),
“mode”: mode,
“settings”: {“pp”: pp, “tg”: tg, “depth”: depth, “runs”: int(runs)},
“models”: ,
}
with open(f, “w”) as fh:
json.dump(data, fh, indent=2)
PYEOF
echo “$CHECKPOINT_FILE” > “$LAST_SESSION_FILE”
}

Mark a model as started (written BEFORE warmup so a crash is detectable)

checkpoint_model_start() {
local model=“$1”
[[ -z “$CHECKPOINT_FILE” ]] && return
python3 - “$CHECKPOINT_FILE” “$model” <<‘PYEOF’
import json, sys, datetime
f, model = sys.argv[1:]
with open(f) as fh:
data = json.load(fh)
data[“models”].append({
“model”: model,
“status”: “started”,
“started_at”: datetime.datetime.now().isoformat(),
})
with open(f, “w”) as fh:
json.dump(data, fh, indent=2)
PYEOF
}

Mark a model as completed with its result (OK / FAIL / INCOHERENT / EMPTY)

checkpoint_model_done() {
local model=“$1”
local result=“$2”
[[ -z “$CHECKPOINT_FILE” ]] && return
python3 - “$CHECKPOINT_FILE” “$model” “$result” <<‘PYEOF’
import json, sys, datetime
f, model, result = sys.argv[1:]
with open(f) as fh:
data = json.load(fh)
for entry in reversed(data[“models”]):
if entry[“model”] == model and entry.get(“status”) == “started”:
entry[“status”] = “completed”
entry[“result”] = result
entry[“completed_at”] = datetime.datetime.now().isoformat()
break
with open(f, “w”) as fh:
json.dump(data, fh, indent=2)
PYEOF
}

Load a previous checkpoint and populate SKIP_MODELS (models already done)

load_resume_checkpoint() {
if [[ ! -f “$LAST_SESSION_FILE” ]]; then
echo -e “${RED}Error: no previous session found. Run without --resume first.${NC}” >&2
exit 1
fi
local prev_cp
prev_cp=$(cat “$LAST_SESSION_FILE”)
if [[ ! -f “$prev_cp” ]]; then
echo -e “${RED}Error: checkpoint file not found: $prev_cp${NC}” >&2
exit 1
fi

log "  ${CYAN}Resuming from checkpoint:${NC} $prev_cp"

local info
info=$(python3 - "$prev_cp" <<'PYEOF'

import json, sys
data = json.load(open(sys.argv[1]))
completed = [e[“model”] for e in data.get(“models”, ) if e.get(“status”) == “completed”]
last_started = next(
(e[“model”] for e in reversed(data.get(“models”, )) if e.get(“status”) == “started”),
“”,
)
s = data.get(“settings”, {})
print(“MODE:” + data.get(“mode”, “”))
print(“SETTINGS:pp=” + str(s.get(“pp”,“”)) + " tg=" + str(s.get(“tg”,“”)) +
" depth=" + str(s.get(“depth”,“”)) + " runs=" + str(s.get(“runs”,“”)))
print(“LAST_STARTED:” + last_started)
for m in completed:
print(“DONE:” + m)
PYEOF
)
while IFS= read -r line; do
case “$line” in
DONE:) SKIP_MODELS+=(“${line#DONE:}”) ;;
MODE:
) log " Previous mode : ${line#MODE:}" ;;
SETTINGS:) log " Previous settings : ${line#SETTINGS:}" ;;
LAST_STARTED:
)
local last=“${line#LAST_STARTED:}”
if [[ -n “$last” ]]; then
log " ${YELLOW}Crashed while running: $last${NC} — will re-run it."
fi
;;
esac
done <<< “$info”

log "  Skipping ${#SKIP_MODELS[@]} already-completed model(s)."
log ""

}

--------------- MAIN ---------------

log “”
log “${BOLD}============================================================${NC}”
log “${BOLD} DGX Spark Model Benchmark${NC}”
log “${BOLD} powered by llama-benchy (GitHub - eugr/llama-benchy: llama-benchy - llama-bench style benchmarking tool for all backends · GitHub)${NC}”
log “${BOLD}============================================================${NC}”
log “”
log " Endpoint : $LLAMA_SWAP_URL"
log " Mode : $MODE"
log " Settings : pp=$PP tg=$TG depth=$DEPTH runs=$RUNS"
log " Date : $(date ‘+%Y-%m-%d %H:%M’)"
log " Results : $RESULTS_DIR"
log “”

Mode descriptions

case “$MODE” in
medium-log)
log " ${CYAN}Profile: Medium Log Baseline${NC}"
log " ${DIM}Simulates a ~50-page document (depth=16384). Establishes your${NC}"
log " ${DIM}baseline generation speed with a moderately full KV cache.${NC}"
;;
stress)
log " ${CYAN}Profile: Massive Log Stress Test${NC}"
log " ${DIM}Doubles context to simulate a massive error log (depth=32768).${NC}"
log " ${DIM}Watch for tg tok/s drop — that’s the unified memory bottleneck.${NC}"
;;
extreme)
log " ${CYAN}Profile: Extreme Limit Test${NC}"
log " ${DIM}Pushes to ~200 pages (depth=65535). Tests if the system can${NC}"
log " ${DIM}process it without crashing or heavy swap paging.${NC}"
;;
quick)
log " ${DIM}Quick smoke test — just checking if models respond.${NC}"
;;
full)
log " ${DIM}Full comprehensive sweep — broad pp/tg/depth combinations.${NC}"
;;
arena)
log " ${CYAN}Profile: Spark-Arena Leaderboard Submission${NC}"
log " ${DIM}Official spark-arena.com benchmark spec: 7 depth levels × 4 concurrency${NC}"
log " ${DIM}levels × 3 runs = 84 data points per model.${NC}"
log " ${DIM}Generates results.csv + recipe.yaml per model in:${NC}"
log " ${DIM} $ARENA_DIR${NC}"
;;
esac
log “”
log " ${DIM}pp = prompt processing (how fast the model reads your input)${NC}"
log " ${DIM}tg = token generation (how fast the model writes its reply)${NC}"
log " ${DIM}depth = pre-filled context tokens (simulates document size)${NC}"
log “”

Initialize crash-resume checkpoint and, if --resume, load previous state

init_checkpoint
[[ “$RESUME” == true ]] && load_resume_checkpoint

Check llama-benchy is available

if ! uvx llama-benchy --help > /dev/null 2>&1; then
log “${RED}Error: llama-benchy not available via uvx.${NC}”
log “Install with: pip install llama-benchy OR uv pip install llama-benchy”
exit 1
fi

Fetch model list

MODELS=$(curl -sf “$LLAMA_SWAP_URL/v1/models” | jq -r ‘.data.id’ | sort)
MODEL_COUNT=$(echo “$MODELS” | wc -l)

Apply filters

if [[ ${#FILTERS[@]} -gt 0 ]]; then
log “Filtering models matching: ${FILTERS[*]}”
FILTERED=“”
for m in $MODELS; do
for f in “${FILTERS[@]}”; do
if [[ “$m” == “$f” ]]; then
FILTERED=“${FILTERED}${m}\n”
fi
done
done
MODELS=$(echo -e “$FILTERED” | grep -v ‘^$’ | sort -u)
MODEL_COUNT=$(echo “$MODELS” | wc -l)
fi

log “Found ${BOLD}${MODEL_COUNT}${NC} model(s) to benchmark.”
log “”

PASS=0
FAIL=0
IDX=0
TOTAL_START=$(date +%s.%N)

Collect results for final summary

declare -A SUMMARY_PP SUMMARY_TG SUMMARY_PEAK SUMMARY_TTFT SUMMARY_STATUS SUMMARY_DEGRADATION

for MODEL in $MODELS; do
IDX=$((IDX + 1))

# Skip models already completed in a --resume session
if [[ "$RESUME" == true ]]; then
    _skip=false
    for _done in "${SKIP_MODELS[@]:-}"; do
        [[ "$MODEL" == "$_done" ]] && { _skip=true; break; }
    done
    if [[ "$_skip" == true ]]; then
        log "  ${DIM}⏭  [$IDX/$MODEL_COUNT] $MODEL — skipped (completed in previous session)${NC}"
        continue
    fi
fi

log "${BOLD}============================================================${NC}"
log "${BOLD}  [$IDX/$MODEL_COUNT] $MODEL${NC}"
log "${BOLD}============================================================${NC}"
log ""

# Write checkpoint BEFORE starting so a crash is detectable
checkpoint_model_start "$MODEL"

# Unload previous model to get a clean measurement
unload_all

# Load this model
WARMUP_FAIL_REASON=""
if ! warmup_model "$MODEL"; then
    FAIL=$((FAIL + 1))
    SUMMARY_STATUS[$MODEL]="${WARMUP_FAIL_REASON:-FAIL}"
    checkpoint_model_done "$MODEL" "${WARMUP_FAIL_REASON:-FAIL}"
    log ""
    continue
fi

# Benchmark it
if run_benchy "$MODEL"; then
    PASS=$((PASS + 1))
    SUMMARY_STATUS[$MODEL]="OK"
    checkpoint_model_done "$MODEL" "OK"
    # In arena mode: generate recipe YAML and check vs personal best
    if [[ "$MODE" == "arena" ]]; then
        generate_recipe_yaml "$MODEL" 2>&1 | tee -a "$REPORT_FILE"
        local arena_json="$RESULTS_DIR/${MODEL//\//_}_${TIMESTAMP}.json"
        local arena_recipe="$ARENA_DIR/${MODEL//\//_}/recipe.yaml"
        check_and_suggest_submit "$MODEL" "$arena_json" "$arena_recipe" 2>&1 | tee -a "$REPORT_FILE"
    fi

    # Extract numbers for final summary from JSON
    local_json="$RESULTS_DIR/${MODEL//\//_}_${TIMESTAMP}.json"
    if [[ -f "$local_json" ]]; then
        # Extract metrics using jq for the summary table
        # We look for depth 0 (baseline) and the deepest result
        baseline_tg=$(jq -r '.benchmarks[] | select(.context_size == 0) | .tg_throughput.mean // empty' "$local_json" | head -n1)
        max_depth=$(jq -r '.benchmarks[].context_size' "$local_json" | sort -rn | head -n1)
        deepest_tg=$(jq -r ".benchmarks[] | select(.context_size == $max_depth) | .tg_throughput.mean // empty" "$local_json" | head -n1)
        
        # Baseline metrics for the summary table
        SUMMARY_PP[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | if .pp_throughput.std > 0.5 then "\(.pp_throughput.mean + 0.5 | floor) +/-\(.pp_throughput.std + 0.5 | floor)" else "\(.pp_throughput.mean + 0.5 | floor)" end' "$local_json" | head -n1)
        SUMMARY_TG[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | if .tg_throughput.std > 0.5 then "\((.tg_throughput.mean * 10 + 0.5 | floor) / 10) +/-\((.tg_throughput.std * 10 + 0.5 | floor) / 10)" else "\((.tg_throughput.mean * 10 + 0.5 | floor) / 10)" end' "$local_json" | head -n1)
        SUMMARY_PEAK[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | .peak_throughput.mean + 0.5 | floor' "$local_json" | head -n1)
        SUMMARY_TTFT[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | .e2e_ttft.mean + 0.5 | floor' "$local_json" | head -n1)
        
        if [[ -n "$baseline_tg" && -n "$deepest_tg" && "$max_depth" -gt 0 ]]; then
            pct=$(echo "scale=1; (($deepest_tg - $baseline_tg) / $baseline_tg) * 100" | bc)
            SUMMARY_DEGRADATION[$MODEL]="${pct}% @$((max_depth/1024))k"
        fi
    fi
else
    FAIL=$((FAIL + 1))
    SUMMARY_STATUS[$MODEL]="FAIL"
    checkpoint_model_done "$MODEL" "FAIL"
fi
log ""

done

Final cleanup

unload_all

TOTAL_END=$(date +%s.%N)
TOTAL_ELAPSED=$(echo “$TOTAL_END - $TOTAL_START” | bc)
TOTAL_MIN=$(echo “scale=1; $TOTAL_ELAPSED / 60” | bc)

=============================================

FINAL REPORT

=============================================

log “”
log “${BOLD}============================================================${NC}”
log “${BOLD} BENCHMARK REPORT — DGX Spark${NC}”
log “${BOLD}============================================================${NC}”
log “”
log " Date: $(date ‘+%Y-%m-%d %H:%M’) | Mode: $MODE | Runs: $RUNS"
log " Depths tested: $DEPTH"
log " Models tested: $MODEL_COUNT | Passed: ${GREEN}$PASS${NC} Failed: ${RED}$FAIL${NC}"
log " Total benchmark time: ${TOTAL_MIN} min"
log " Checkpoint : $CHECKPOINT_FILE"
[[ $FAIL -gt 0 ]] && log " ${YELLOW}Tip: if this was interrupted, resume with: ./benchmark-models.sh --resume${NC}"
log “”
log " ${BOLD}$(printf ‘%-42s %14s %12s %8s %8s %14s’ ‘Model’ ‘Read (pp)’ ‘Write (tg)’ ‘Peak’ ‘TTFT’ ‘Deep ctx’)${NC}"
log " ${DIM}$(printf ‘%-42s %14s %12s %8s %8s %14s’ ‘’ ‘tok/s’ ‘tok/s’ ‘tok/s’ ‘ms’ ‘degradation’)${NC}"
log " $(printf ‘%.0s-’ {1..106})"

for MODEL in $MODELS; do
local_name=“$MODEL”
[[ ${#local_name} -gt 42 ]] && local_name=“${local_name:0:39}…”

status="${SUMMARY_STATUS[$MODEL]:-FAIL}"
pp="${SUMMARY_PP[$MODEL]:-—}"
tg="${SUMMARY_TG[$MODEL]:-—}"
peak="${SUMMARY_PEAK[$MODEL]:-—}"
ttft="${SUMMARY_TTFT[$MODEL]:-—}"
degrad="${SUMMARY_DEGRADATION[$MODEL]:-—}"

if [[ "$status" == "OK" ]]; then
    printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "$pp" "$tg" "$peak" "$ttft" "$degrad"
    log "${GREEN}${line}${NC}"
elif [[ "$status" == "INCOHERENT" ]]; then
    printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "INCOHERENT" "—" "—" "—" "—"
    log "${YELLOW}${line}${NC}"
else
    printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "FAIL" "—" "—" "—" "—"
    log "${RED}${line}${NC}"
fi

done

log “”
log " ${DIM}---------------------------------------------------------------${NC}"
log " ${DIM}How to read this table:${NC}"
log " ${DIM}${NC}"
log " ${DIM} Read (pp) = How fast the model reads your prompt (higher = better).${NC}"
log " ${DIM} Write (tg) = How fast the model types its answer at depth=0 baseline.${NC}"
log " ${DIM} This is the speed you feel when chatting.${NC}"
log " ${DIM} Humans read at ~4 tok/s, so 20+ feels smooth.${NC}"
log " ${DIM} Peak = Fastest burst speed observed in a 1-second window.${NC}"
log " ${DIM} TTFT = Time until the first word appears (lower = better).${NC}"
log " ${DIM} Deep ctx = Speed change at max tested depth vs baseline.${NC}"
log " ${DIM} >-15% = unified memory bandwidth bottleneck.${NC}"
log " ${DIM}---------------------------------------------------------------${NC}"
log “”
log " Results saved to:"
log " Report : $REPORT_FILE"
log " JSON data : $RESULTS_DIR/_${TIMESTAMP}.json"
log " Forum tables: $RESULTS_DIR/
_${TIMESTAMP}.md"
log “”
log " ${DIM}Tip: To share on NVIDIA forums, copy the llama-benchy tables${NC}"
log " ${DIM}from the .md files — they use the standard format everyone knows.${NC}"

if [[ “$MODE” == “arena” && -n “$ARENA_DIR” ]]; then
log “”
log “${BOLD}============================================================${NC}”
log “${BOLD} SPARK-ARENA SUBMISSION FILES${NC}”
log “${BOLD}============================================================${NC}”
log “”
log " Submission directory: ${CYAN}$ARENA_DIR${NC}"
log “”
log " Per-model folders (one submission per model):"
for MODEL in $MODELS; do
safe=“${MODEL////_}”
model_dir=“$ARENA_DIR/$safe”
if [[ -f “$model_dir/results.csv” && -f “$model_dir/recipe.yaml” ]]; then
log " ${GREEN}✓${NC} $MODEL"
log " recipe.yaml : $model_dir/recipe.yaml"
log " results.csv : $model_dir/results.csv"
elif [[ “${SUMMARY_STATUS[$MODEL]:-FAIL}” != “OK” ]]; then
log " ${RED}✗${NC} $MODEL (benchmark failed — no submission files)"
fi
done
log “”
log " ${BOLD}How to submit to spark-arena.com/admin:${NC}"
log " ${DIM}1. Open https://spark-arena.com/admin${NC}"
log " ${DIM}2. For each model folder above:${NC}"
log " ${DIM} a. Paste or upload the contents of recipe.yaml${NC}"
log " ${DIM} b. Upload results.csv${NC}"
log " ${DIM}3. Submit one entry per model.${NC}"
log “”
log " ${DIM}Note: The recipe.yaml ‘model:’ field uses the canonical HuggingFace ID.${NC}"
log " ${DIM}If your model was downloaded from a different source, update it.${NC}"
fi
log “${BOLD}============================================================${NC}”

`

Full stack is LiteLLM → llama-swap → vLLM/llama.cpp, benchmark script is at: benchmark-models.sh in the repo — drop it next to your llama-swap config and it auto-discovers all registered models.

Requires uvx (comes with uv) + llama-benchy. No other deps.

Cool project.

Still might be good for you to consider using sparkrun for arena benchmarks because sparkrun also supports login and upload of results, so that’s one step. (Assuming the person has done login via sparkrun). We can talk offline about integration because sparkrun does let you benchmark a model that’s already running (hidden option for arena benchmarks, but it’s implemented), so you could just point sparkrun at the recipe file and have it run the benchmark and upload directly to spark arena (it’ll still save local copies of files that you could use for local work and arena-best-results.json, etc.). That could basically replace the spark-arena-cli part… plus sparkrun collects other metadata to try to properly classify and register versions of CUDA, NCCL, vllm, etc. as part of the benchmark data. It’s not all exposed today on spark-arena.com, but benchmarks uploaded via sparkrun have richer metadata that can be used to properly identify quants, pin specific versions of spark-vllm-docker for future reproduction, and understand changes over time from vllm version, etc.

Also, FYI. sparkrun does have a litellm proxy functionality that supports load/unload and dynamically assigning ports to avoid collisions, etc. That being said, you still need to choose what to unload – it doesn’t act as a scheduler to decide what to put where, etc.

Anyway, we can talk offline if it makes sense to integrate or not.

VLLM_ALLOW_LONG_MAX_MODEL_LEN=1 VLLM_WORKER_MULTIPROC_METHOD=spawn vllm serve     --model /llm/models/DeepSeek-R1-Distill-Qwen-7B     --served-model-name DeepSeek-R1-Distill-Qwen-7B     --dtype=float16     --enforce-eager     --port 8000     --host 0.0.0.0     --trust-remote-code     --disable-sliding-window     --gpu-memory-util=0.9     --disable-log-requests   --block-size 64     --quantization fp8     -tp=1


bash benchmark-models.sh DeepSeek-R1-Distill-Qwen-7B

============================================================
  DGX Spark Model Benchmark
  powered by llama-benchy (github.com/eugr/llama-benchy)
============================================================

  Endpoint : http://localhost:8000
  Mode     : medium-log
  Settings : pp=2048  tg=128  depth=0 16384  runs=3
  Date     : 2026-04-21 01:26
  Results  : /home/intel/Downloads/vllm/test-results/benchmarks

  Profile: Medium Log Baseline
  Simulates a ~50-page document (depth=16384). Establishes your
  baseline generation speed with a moderately full KV cache.

  pp = prompt processing (how fast the model reads your input)
  tg = token generation  (how fast the model writes its reply)
  depth = pre-filled context tokens (simulates document size)

Error: llama-benchy not available via uvx.
Install with: pip install llama-benchy  OR  uv pip install llama-benchy
(.venv) intel@P7:~/Downloads/vllm$ llama-benchy
PyTorch was not found. Models won't be available and only tokenizers, configuration and file/data utilities can be used.
usage: llama-benchy [-h] [--version] --base-url BASE_URL [--api-key API_KEY] [--model MODEL] [--served-model-name SERVED_MODEL_NAME] [--tokenizer TOKENIZER] [--pp PP [PP ...]] [--tg TG [TG ...]] [--depth DEPTH [DEPTH ...]] [--runs RUNS] [--no-cache] [--post-run-cmd POST_RUN_CMD]
                    [--book-url BOOK_URL] [--latency-mode {api,generation,none}] [--no-warmup] [--skip-coherence] [--adapt-prompt] [--no-adapt-prompt] [--enable-prefix-caching] [--concurrency CONCURRENCY [CONCURRENCY ...]] [--save-result SAVE_RESULT] [--format {md,json,csv}]
                    [--save-total-throughput-timeseries] [--save-all-throughput-timeseries] [--exit-on-first-fail] [--no-results-on-fail]
llama-benchy: error: the following arguments are required: --base-url

If vLLM is calling a model that has already been downloaded into a specified local folder, the script should be able to handle it correctly.

Can I use locale tokenizer ? · Issue #7 · eugr/llama-benchy

pip show transformers
Name: transformers
Version: 5.5.4
Summary: Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training.
Home-page: GitHub - huggingface/transformers: 🤗 Transformers: the model-definition framework for state-of-the-art machine learning models in text, vision, audio, and multimodal models, for both inference and training. · GitHub
Author: The Hugging Face team (past and future) with the help of all our contributors (Contributors to huggingface/transformers · GitHub)
Author-email: transformers@huggingface.co
License: Apache 2.0 License
Location: /home/intel/Downloads/tool-eval-bench/.venv/lib/python3.13/site-packages
Requires: huggingface-hub, numpy, packaging, pyyaml, regex, safetensors, tokenizers, tqdm, typer
Required-by: llama-benchy

thanks cho for the feedback.

llama-benchy is installed directly in our venv but not uvx .
The script needs to detect whichever is available.

The script now tries in order:

  1. uvx llama-benchy — “my” setup, no install needed
  2. llama-benchy — the “your” setup (pip-installed in venv)
  3. Fails with clear instructions if neither is found

download the update from github

or copy the updated script benchmark-models.sh here
# =============================================================================
# benchmark-models.sh — Benchmark all llama-swap models using llama-benchy
#
# Uses llama-benchy (https://github.com/eugr/llama-benchy) for standardized
# LLM performance measurement. Results are comparable with other DGX Spark
# users who use the same tool.
#
# Benchmark profiles simulate real-world document analysis workloads:
#
#   "Medium Log"  (default)  — 50-page document, pp2048 + tg128 @ depth 16384
#   "Massive Log" (--stress) — 100+ page log,    pp2048 + tg128 @ depth 32768
#   "Extreme"     (--extreme)— 200+ page corpus,  pp2048 + tg128 @ depth 65535
#
# Each profile includes a depth=0 baseline so you can see the performance
# delta as unified memory pressure increases.
#
# Usage:
#   ./benchmark-models.sh                        # Medium Log (default)
#   ./benchmark-models.sh --stress               # Medium + Massive Log
#   ./benchmark-models.sh --extreme              # All three depth levels
#   ./benchmark-models.sh --quick Nemotron       # Fast smoke test
#   ./benchmark-models.sh --runs 5 Qwen3.5-35B   # Custom run count
#   ./benchmark-models.sh Qwen3.5 Nemotron       # Only matching models
# =============================================================================

set -euo pipefail

LLAMA_SWAP_URL="${LLAMA_SWAP_URL:-http://localhost:28080}"
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
RESULTS_DIR="$SCRIPT_DIR/test-results/benchmarks"
ARENA_BEST_FILE="$SCRIPT_DIR/test-results/arena-best-results.json"
TIMEOUT=1800

# spark-arena-cli: installed to ~/.local/bin on first --arena run
SPARK_CLI="${HOME}/.local/bin/spark-arena-cli"

# Colors
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# Defaults: "Medium Log" baseline
PP="2048"
TG="128"
DEPTH="0 16384"
RUNS=3
MODE="medium-log"
CONCURRENCY=""   # space-separated list; empty = not passed to llama-benchy
ARENA_DIR=""     # set by --arena mode
FILTERS=()

# Crash-resume tracking
CHECKPOINT_DIR="$SCRIPT_DIR/test-results/checkpoints"
LAST_SESSION_FILE="$SCRIPT_DIR/test-results/.last-session"
CHECKPOINT_FILE=""
RESUME=false
SKIP_MODELS=()

# Parse arguments
while [[ $# -gt 0 ]]; do
    case "$1" in
        --quick)
            PP="512"
            TG="128"
            DEPTH="0"
            RUNS=1
            MODE="quick"
            shift
            ;;
        --stress)
            # Medium Log + Massive Log
            PP="2048"
            TG="128"
            DEPTH="0 16384 32768"
            RUNS=3
            MODE="stress"
            shift
            ;;
        --extreme)
            # All three: Medium + Massive + Extreme limit
            PP="2048"
            TG="128"
            DEPTH="0 16384 32768 65535"
            RUNS=3
            MODE="extreme"
            shift
            ;;
        --full)
            # Comprehensive sweep (original broad test)
            PP="512 2048"
            TG="128 256 512"
            DEPTH="0 16384 32768"
            RUNS=3
            MODE="full"
            shift
            ;;
        --arena)
            # Spark-Arena leaderboard submission profile
            # https://spark-arena.com/admin
            PP="2048"
            TG="128"
            DEPTH="0 4096 8192 16384 32768 65535 100000"
            CONCURRENCY="1 2 5 10"
            RUNS=3
            MODE="arena"
            shift
            ;;
        --runs)
            RUNS="$2"
            shift 2
            ;;
        --runs=*)
            RUNS="${1#*=}"
            shift
            ;;
        --resume)
            RESUME=true
            shift
            ;;
        --help|-h)
            cat <<'HELPEOF'
Usage: benchmark-models.sh [OPTIONS] [FILTER...]

Benchmark your llama-swap models using llama-benchy.
Results use the standard llama-benchy format for comparison
with other DGX Spark users on the NVIDIA forums.

Profiles (simulating real-world document workloads):

  (default)    "Medium Log" — pp2048, tg128 @ depth 0 + 16384
               Simulates a ~50-page document. The depth=0 baseline shows
               raw speed; depth=16384 shows the cost of a full KV cache.

  --stress     "Massive Log" — adds depth 32768
               Doubles the context to simulate a massive error log.
               Watch for tg tok/s drop vs. the medium baseline — that's
               where shared memory bandwidth starts to bottleneck.

  --extreme    "Extreme Limit" — adds depth 65535
               Pushes to ~100 pages of text. Strictly to see if the
               system can process it without crashing or heavy swap.

  --quick      Smoke test — pp512, tg128, depth 0, 1 run
  --full       Broad sweep — pp512+2048, tg128+256+512, depths 0-32k
  --arena      Spark-Arena leaderboard profile — exact spec from spark-arena.com/admin
               Saves results.csv + recipe.yaml per model to test-results/arena-submission/
               Depths: 0 4096 8192 16384 32768 65535 100000 | Concurrency: 1 2 5 10

Other options:
  --runs N     Override number of runs (default: 3)
  --resume     Resume from the last interrupted session (skip already-completed models)
  --help       Show this help

Filters:
  Add model name fragments to only test matching models.
  Example: ./benchmark-models.sh --stress Qwen3.5 Nemotron

Environment:
  LLAMA_SWAP_URL  llama-swap endpoint (default: http://localhost:28080)

What the numbers mean:
  pp tok/s  = Prompt Processing speed. How fast the model reads your input.
              Higher is better. Typically 500-5000+ tok/s on DGX Spark.
  tg tok/s  = Token Generation speed. How fast the model writes its reply.
              Higher is better. This is the number you "feel" when chatting.
              Typically 15-50+ tok/s on DGX Spark depending on model size.
  TTFT      = Time To First Token. The delay before the model starts replying.
              Lower is better. Measured in milliseconds.

Key insight:
  Compare tg tok/s across depths. A big drop from depth=16384 to depth=32768
  means you've found the unified memory bandwidth bottleneck between the ARM
  CPU and the Blackwell GPU on DGX Spark.
HELPEOF
            exit 0
            ;;
        *)
            FILTERS+=("$1")
            shift
            ;;
    esac
done

mkdir -p "$RESULTS_DIR"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
REPORT_FILE="$RESULTS_DIR/report_${TIMESTAMP}.txt"

if [[ "$MODE" == "arena" ]]; then
    ARENA_DIR="$SCRIPT_DIR/test-results/arena-submission/${TIMESTAMP}"
    mkdir -p "$ARENA_DIR"
fi

log() { echo -e "$1" | tee -a "$REPORT_FILE"; }

unload_all() {
    curl -sf -X POST "$LLAMA_SWAP_URL/unload" > /dev/null 2>&1 || true
    sleep 5
}

# Warm up: send a tiny request to make llama-swap load the model
warmup_model() {
    local model="$1"
    log "  Loading model via llama-swap..."
    local start end elapsed
    start=$(date +%s.%N)

    local response_file="/tmp/response_${TIMESTAMP}.json"
    local http_code
    http_code=$(curl -s -w "%{http_code}" -o "$response_file" --max-time "$TIMEOUT" \
        -X POST "$LLAMA_SWAP_URL/v1/chat/completions" \
        -H "Content-Type: application/json" \
        -d "$(jq -n --arg model "$model" '{
            model: $model,
            messages: [{role: "user", content: "Write a short python hello world script."}],
            max_tokens: 50
        }')" 2>/dev/null) || http_code=0

    end=$(date +%s.%N)
    elapsed=$(echo "scale=1; $end - $start" | bc)

    if [[ "$http_code" -ne 200 ]]; then
        local err_msg
        err_msg=$(jq -r '.error.message // empty' "$response_file" 2>/dev/null)
        if [[ -n "$err_msg" ]]; then
            log "  ${RED}FAILED to load: $err_msg (HTTP $http_code)${NC}"
        else
            log "  ${RED}FAILED to load (HTTP $http_code)${NC}"
        fi
        rm -f "$response_file"
        return 1
    fi

    local content
    content=$(jq -r '(.choices[0].message.reasoning_content // "") + (.choices[0].message.reasoning // "") + (.choices[0].message.content // "")' "$response_file" 2>/dev/null | tr -d '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
    rm -f "$response_file"

    if [[ -z "$content" ]]; then
        log "  ${CYAN}Coherence check:${NC} ${RED}FAILED — empty response${NC}"
        WARMUP_FAIL_REASON="EMPTY"
        return 1
    fi

    # Detect repetition loop: split into words, check if any single word
    # makes up >60% of total words (e.g. "n8n n8n n8n..." or "the the the...")
    local word_count most_freq_count most_freq_word
    word_count=$(echo "$content" | wc -w)
    if [[ "$word_count" -ge 5 ]]; then
        most_freq_word=$(echo "$content" | tr ' ' '\n' | sort | uniq -c | sort -rn | awk 'NR==1{print $2}')
        most_freq_count=$(echo "$content" | tr ' ' '\n' | grep -cFx "$most_freq_word" 2>/dev/null || echo 0)
        local pct=$(( most_freq_count * 100 / word_count ))
        if [[ "$pct" -ge 60 ]]; then
            log "  ${CYAN}Coherence check:${NC} ${RED}FAILED — repetition loop (\"${most_freq_word}\" = ${pct}% of output)${NC}"
            log "  ${RED}Skipping benchmark — model output is incoherent.${NC}"
            WARMUP_FAIL_REASON="INCOHERENT"
            return 1
        fi
    fi

    log "  ${CYAN}Coherence check:${NC} \"${content:0:150}\""
    log "  Model ready (loaded in ${elapsed}s)"
    WARMUP_FAIL_REASON=""
    return 0
}

# Run llama-benchy and capture results
run_benchy() {
    local model="$1"
    local safe_name="${model//\//_}"
    local json_file="$RESULTS_DIR/${safe_name}_${TIMESTAMP}.json"
    local md_file="$RESULTS_DIR/${safe_name}_${TIMESTAMP}.md"

    # Show profile description
    case "$MODE" in
        medium-log) log "  Profile: Medium Log (50-page document baseline)" ;;
        stress)     log "  Profile: Massive Log (stress test — watch for bandwidth bottleneck)" ;;
        extreme)    log "  Profile: Extreme Limit (push to ~100 pages, crash/swap detection)" ;;
        quick)      log "  Profile: Quick smoke test" ;;
        full)       log "  Profile: Full comprehensive sweep" ;;
        arena)      log "  Profile: Spark-Arena leaderboard (7 depths × 4 concurrency levels)" ;;
    esac
    local concurrency_display=""
    [[ -n "$CONCURRENCY" ]] && concurrency_display="  concurrency=$CONCURRENCY"
    log "  Running llama-benchy (pp=$PP  tg=$TG  depth=$DEPTH  runs=$RUNS${concurrency_display})..."
    log ""

    # Build shared base flags (used by all runs)
    local base_flags=""
    base_flags+=" --base-url $LLAMA_SWAP_URL/v1"
    base_flags+=" --model $model"
    base_flags+=" --pp $PP"
    base_flags+=" --tg $TG"
    base_flags+=" --depth $DEPTH"
    base_flags+=" --runs $RUNS"
    base_flags+=" --latency-mode generation"
    base_flags+=" --no-warmup"
    base_flags+=" --skip-coherence"
    [[ -n "$CONCURRENCY" ]] && base_flags+=" --concurrency $CONCURRENCY"
    [[ "$MODE" == "arena" ]] && base_flags+=" --enable-prefix-caching"

    # --- Run 1: Save JSON for data parsing ---
    local cmd_json="${BENCHY_CMD}${base_flags} --save-result ${json_file} --format json"

    local output exit_code=0
    output=$(eval "$cmd_json" 2>&1) || exit_code=$?

    if [[ $exit_code -ne 0 ]]; then
        log "  ${RED}llama-benchy failed:${NC}"
        echo "$output" | tail -15 | tee -a "$REPORT_FILE"
        return 1
    fi

    # --- Arena mode: save submission CSV (separate run, same params) ---
    if [[ "$MODE" == "arena" && -n "$ARENA_DIR" ]]; then
        local arena_model_dir="$ARENA_DIR/${safe_name}"
        mkdir -p "$arena_model_dir"
        local csv_file="$arena_model_dir/results.csv"
        local cmd_csv="${BENCHY_CMD}${base_flags} --save-result ${csv_file} --format csv"
        log "  ${CYAN}Saving arena submission CSV...${NC}"
        local csv_output csv_exit=0
        csv_output=$(eval "$cmd_csv" 2>&1) || csv_exit=$?
        if [[ $csv_exit -ne 0 ]]; then
            log "  ${YELLOW}Warning: CSV run failed — JSON data still saved${NC}"
        else
            log "  ${DIM}Arena CSV  : $csv_file${NC}"
        fi
    fi

    # --- Run 2: Get the markdown table (for sharing on forums) ---
    local cmd_md="${BENCHY_CMD}${base_flags} --save-result ${md_file} --format md"

    local md_output
    md_output=$(eval "$cmd_md" 2>&1) || true

    # Show the full saved markdown file (the format people share on forums)
    if [[ -f "$md_file" ]]; then
        log "  ${CYAN}llama-benchy results (copy this to share on forums):${NC}"
        log ""
        cat "$md_file" | tee -a "$REPORT_FILE"
        log ""
    else
        # Fallback: show table lines from stdout if file wasn't created
        local table_lines
        table_lines=$(echo "$md_output" | grep -E '^\|')
        if [[ -n "$table_lines" ]]; then
            log "  ${CYAN}llama-benchy results:${NC}"
            log ""
            echo "$table_lines" | tee -a "$REPORT_FILE"
            log ""
        fi
    fi

    # --- Parse JSON and show friendly explanation ---
    if [[ -f "$json_file" ]]; then
        python3 <<PYEOF | tee -a "$REPORT_FILE"
import json

with open('$json_file') as f:
    data = json.load(f)

benchmarks = data.get('benchmarks', [])
if not benchmarks:
    print("  (no benchmark data found)")
    exit(0)

# Check if all benchmarks have null results (model failed silently)
all_null = all(
    b.get('pp_throughput') is None and b.get('tg_throughput') is None
    for b in benchmarks
)
if all_null:
    print("  (model returned no usable results — it may not support this benchmark)")
    exit(0)

# Group benchmarks by depth for comparison
depth_results = {}
for b in benchmarks:
    depth = b.get('context_size', 0)
    pp_obj  = b.get('pp_throughput') or {}
    tg_obj  = b.get('tg_throughput') or {}
    pk_obj  = b.get('peak_throughput') or {}
    e2e_obj = b.get('e2e_ttft') or {}
    pp_mean  = pp_obj.get('mean', 0) or 0
    tg_mean  = tg_obj.get('mean', 0) or 0
    if pp_mean > 0 or tg_mean > 0:
        depth_results[depth] = {
            'pp_mean': pp_mean, 'pp_std': (pp_obj.get('std', 0) or 0),
            'tg_mean': tg_mean, 'tg_std': (tg_obj.get('std', 0) or 0),
            'pk_mean': (pk_obj.get('mean', 0) or 0),
            'e2e_mean': (e2e_obj.get('mean', 0) or 0),
        }

# Depth label mapping
depth_labels = {
    0: "Baseline (no context)",
    16384: "Medium Log (~50 pages)",
    32768: "Massive Log (~100 pages)",
    65535: "Extreme Limit (~200 pages)",
}

print("  Context Depth Analysis:")
print("")

baseline_tg = None
for depth in sorted(depth_results.keys()):
    r = depth_results[depth]
    label = depth_labels.get(depth, f"depth {depth}")
    pp_mean, pp_std = r['pp_mean'], r['pp_std']
    tg_mean, tg_std = r['tg_mean'], r['tg_std']
    pk_mean = r['pk_mean']
    e2e_mean = r['e2e_mean']

    print(f"  --- depth={depth:,} — {label} ---")

    # Reading speed
    if pp_mean > 0:
        if pp_std > 0.5:
            print(f"    Reading speed:  {pp_mean:,.0f} +/- {pp_std:,.0f} tok/s")
        else:
            print(f"    Reading speed:  {pp_mean:,.0f} tok/s")

    # Writing speed
    if tg_mean > 0:
        if tg_std > 0.5:
            print(f"    Writing speed:  {tg_mean:.1f} +/- {tg_std:.1f} tok/s  (peak: {pk_mean:.0f})")
        else:
            print(f"    Writing speed:  {tg_mean:.1f} tok/s  (peak: {pk_mean:.0f})")

        # Show degradation from baseline
        if baseline_tg is None:
            baseline_tg = tg_mean
        elif baseline_tg > 0:
            pct = ((tg_mean - baseline_tg) / baseline_tg) * 100
            if pct < -15:
                print(f"    {chr(9888)}  {pct:+.1f}% vs baseline — BANDWIDTH BOTTLENECK DETECTED")
            elif pct < -5:
                print(f"      -> {pct:+.1f}% vs baseline (moderate slowdown)")
            else:
                print(f"      -> {pct:+.1f}% vs baseline (minimal impact)")

        # What it feels like
        if tg_mean >= 40:
            feel = "Very fast — feels instant, smooth streaming"
        elif tg_mean >= 25:
            feel = "Fast — comfortable for interactive chat"
        elif tg_mean >= 15:
            feel = "Good — readable streaming with slight pauses"
        elif tg_mean >= 8:
            feel = "Moderate — noticeable wait, but usable"
        else:
            feel = "Slow — may feel sluggish for chat"
        print(f"      -> {feel}")

    # TTFT
    if e2e_mean > 0:
        print(f"    Time to first token:  {e2e_mean:.0f}ms")
        if e2e_mean < 200:
            print(f"      -> Feels instant")
        elif e2e_mean < 500:
            print(f"      -> Barely noticeable delay")
        elif e2e_mean < 2000:
            print(f"      -> Short pause before response starts")
        else:
            print(f"      -> Noticeable wait ({e2e_mean/1000:.1f}s)")
    print("")

# Summary comparison if multiple depths
if len(depth_results) > 1 and baseline_tg and baseline_tg > 0:
    max_depth = max(depth_results.keys())
    deepest_tg = depth_results[max_depth]['tg_mean']
    total_pct = ((deepest_tg - baseline_tg) / baseline_tg) * 100
    print(f"  Overall impact: depth 0 -> {max_depth:,} = {total_pct:+.1f}% generation speed")
    if total_pct < -20:
        print(f"  Conclusion: Significant unified memory bandwidth bottleneck at depth {max_depth:,}")
    elif total_pct < -10:
        print(f"  Conclusion: Moderate bandwidth pressure — usable but noticeably slower")
    else:
        print(f"  Conclusion: Model handles deep context well on this hardware")
    print("")

PYEOF
    fi

    log "  ${DIM}JSON data : $json_file${NC}"
    log "  ${DIM}Forum table: $md_file${NC}"
    return 0
}

# ---------------------------------------------------------------------------
# spark-arena-cli helpers
# ---------------------------------------------------------------------------

# Download spark-arena-cli binary if it isn't already on PATH / ~/.local/bin
install_spark_arena_cli() {
    if command -v spark-arena-cli &>/dev/null; then
        SPARK_CLI="$(command -v spark-arena-cli)"
        return 0
    fi
    if [[ -x "$SPARK_CLI" ]]; then
        return 0
    fi
    local arch
    arch=$(uname -m)
    local bin_name
    [[ "$arch" == "aarch64" || "$arch" == "arm64" ]] \
        && bin_name="spark-arena-cli-0.1.0-linux-arm64" \
        || bin_name="spark-arena-cli-0.1.0-linux-amd64"
    local url="https://github.com/spark-arena/spark-arena-cli/releases/download/v0.1.0/${bin_name}"
    log "  ${DIM}Downloading spark-arena-cli from GitHub releases...${NC}"
    mkdir -p "$(dirname "$SPARK_CLI")"
    if curl -fsSL "$url" -o "$SPARK_CLI" 2>/dev/null; then
        chmod +x "$SPARK_CLI"
        log "  ${DIM}Installed to $SPARK_CLI${NC}"
        # Add to PATH for this session
        export PATH="$(dirname "$SPARK_CLI"):$PATH"
    else
        log "  ${YELLOW}Warning: could not download spark-arena-cli — manual install needed${NC}"
        SPARK_CLI=""
    fi
}

# Read our personal best tg tok/s (depth=0, concurrency=1) for a model from the history file
get_personal_best_tg() {
    local model="$1"
    if [[ ! -f "$ARENA_BEST_FILE" ]]; then echo "0"; return; fi
    python3 -c "
import json, sys
try:
    d = json.load(open('$ARENA_BEST_FILE'))
    entry = d.get('$model', {})
    print(entry.get('tg_mean', 0))
except:
    print(0)
" 2>/dev/null || echo "0"
}

# Save current result as personal best for a model
save_personal_best() {
    local model="$1"
    local json_file="$2"
    python3 - "$model" "$json_file" "$ARENA_BEST_FILE" <<'PYEOF'
import json, sys, os
model, bench_json, best_file = sys.argv[1], sys.argv[2], sys.argv[3]

try:
    data = json.load(open(bench_json))
except Exception as e:
    sys.exit(0)

# Find depth=0, concurrency=1 entry
baseline = None
for b in data.get("benchmarks", []):
    if b.get("context_size") == 0 and b.get("concurrency") == 1:
        baseline = b
        break
if not baseline:
    # Fallback: first entry with context_size=0
    for b in data.get("benchmarks", []):
        if b.get("context_size") == 0:
            baseline = b
            break
if not baseline:
    sys.exit(0)

tg  = (baseline.get("tg_throughput")  or {}).get("mean", 0) or 0
pp  = (baseline.get("pp_throughput")  or {}).get("mean", 0) or 0
e2e = (baseline.get("e2e_ttft")       or {}).get("mean", 0) or 0

try:
    best = json.load(open(best_file)) if os.path.exists(best_file) else {}
except:
    best = {}

best[model] = {
    "tg_mean": round(tg, 2),
    "pp_mean": round(pp, 1),
    "ttft_ms": round(e2e, 1),
    "timestamp": data.get("timestamp", ""),
    "depth": 0,
    "concurrency": 1,
}
with open(best_file, "w") as f:
    json.dump(best, f, indent=2)
PYEOF
}

# Compare current result vs personal best; if better, print submission info
check_and_suggest_submit() {
    local model="$1"
    local json_file="$2"
    local recipe_file="$3"

    local prev_best
    prev_best=$(get_personal_best_tg "$model")

    local current_tg
    current_tg=$(python3 -c "
import json
data = json.load(open('$json_file'))
for b in data.get('benchmarks', []):
    if b.get('context_size') == 0 and b.get('concurrency') == 1:
        tg = (b.get('tg_throughput') or {}).get('mean', 0) or 0
        print(round(tg, 2))
        exit()
for b in data.get('benchmarks', []):
    if b.get('context_size') == 0:
        tg = (b.get('tg_throughput') or {}).get('mean', 0) or 0
        print(round(tg, 2))
        exit()
print(0)
" 2>/dev/null || echo "0")

    local is_better=0
    python3 -c "exit(0 if float('$current_tg') > float('$prev_best') else 1)" 2>/dev/null && is_better=1

    if [[ "$is_better" -eq 1 ]]; then
        if python3 -c "exit(0 if float('$prev_best') > 0 else 1)" 2>/dev/null; then
            log "  ${GREEN}★ NEW PERSONAL BEST${NC} — ${current_tg} tg tok/s (was ${prev_best})"
        else
            log "  ${GREEN}★ FIRST ARENA RESULT${NC} — ${current_tg} tg tok/s @ depth=0 concurrency=1"
        fi
        save_personal_best "$model" "$json_file"

        # Try to get leaderboard context (best-effort scrape — may be empty)
        log "  ${CYAN}Submission candidate${NC} — consider submitting to spark-arena.com/leaderboard"
        log ""

        install_spark_arena_cli

        if [[ -n "$SPARK_CLI" && -x "$SPARK_CLI" ]]; then
            local is_logged_in=0
            "$SPARK_CLI" benchmark --help &>/dev/null && {
                # Probe login state: spark-arena-cli prints a warning if not configured
                local probe
                probe=$(echo "exit" | timeout 3 "$SPARK_CLI" 2>&1 || true)
                echo "$probe" | grep -q "Warning: Configuration not found" || is_logged_in=1
            }

            if [[ "$is_logged_in" -eq 1 ]]; then
                log "  ${GREEN}spark-arena-cli is logged in.${NC} Run this to submit officially:"
                log "  ${BOLD}  $SPARK_CLI benchmark $recipe_file${NC}"
                log "  ${DIM}  (This re-runs the benchmark via sparkrun and auto-uploads results)${NC}"
            else
                log "  ${YELLOW}spark-arena-cli installed but not logged in.${NC} To submit:"
                log "  ${DIM}  1. $SPARK_CLI login         # authenticate via Google/GitHub${NC}"
                log "  ${DIM}  2. $SPARK_CLI setup         # configure sparkrun + llama-benchy${NC}"
                log "  ${DIM}  3. $SPARK_CLI benchmark $recipe_file${NC}"
            fi
        else
            log "  ${DIM}To submit to spark-arena, install spark-arena-cli:${NC}"
            log "  ${DIM}  curl -fsSL https://github.com/spark-arena/spark-arena-cli/releases/download/v0.1.0/spark-arena-cli-0.1.0-linux-amd64 -o ~/.local/bin/spark-arena-cli && chmod +x ~/.local/bin/spark-arena-cli${NC}"
            log "  ${DIM}  spark-arena-cli login${NC}"
            log "  ${DIM}  spark-arena-cli benchmark $recipe_file${NC}"
        fi
        log ""
    else
        log "  ${DIM}tg ${current_tg} tok/s — personal best is ${prev_best} tok/s (no improvement, skipping submission)${NC}"
    fi
}

# Generate a spark-arena recipe.yaml for a model
generate_recipe_yaml() {
    local model="$1"
    local safe_name="${model//\//_}"
    local out_dir="$ARENA_DIR/${safe_name}"
    mkdir -p "$out_dir"

    python3 - "$model" "$out_dir/recipe.yaml" <<'PYEOF'
import sys, textwrap

model_name = sys.argv[1]
out_path   = sys.argv[2]

# Per-model recipe metadata.
# container: the local Docker image tag we actually use.
# hf_model: canonical HuggingFace model ID for the submission.
RECIPES = {
    "Qwen3.5-35B-A3B-FP8": {
        "hf_model":    "Qwen/Qwen3.5-35B-A3B-Instruct",
        "description": "Qwen3.5 35B MoE FP8-dynamic — reasoning + tool use with MTP-2 speculation",
        "container":   "vllm-node:Version_1",
        "tp": 1, "gpu_mem": 0.7, "max_len": 131072,
        "env": {
            "VLLM_MARLIN_USE_ATOMIC_ADD": "1",
            "VLLM_ENABLE_CUDAGRAPH_GC": "1",
            "VLLM_USE_FLASHINFER_SAMPLER": "1",
        },
        "extras": [
            "--kv-cache-dtype fp8",
            "--load-format fastsafetensors",
            "--attention-backend FLASHINFER",
            "--enable-prefix-caching",
            "--enable-chunked-prefill",
            "--max-num-batched-tokens 4096",
            '--speculative-config \'{"method":"mtp","num_speculative_tokens":2}\'',
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_xml",
            "--reasoning-parser qwen3",
        ],
    },
    "Qwen3.5-122B-A10B-int4-AutoRound": {
        "hf_model":    "Qwen/Qwen3.5-122B-A10B-Instruct",
        "description": "Qwen3.5 122B MoE INT4 AutoRound — large hybrid reasoning model",
        "container":   "vllm-node-tf5:latest",
        "tp": 1, "gpu_mem": 0.75, "max_len": 40960,
        "env": {"VLLM_MARLIN_USE_ATOMIC_ADD": "1"},
        "extras": [
            "--trust-remote-code",
            "--enforce-eager",
            "--kv-cache-dtype fp8",
            "--enable-prefix-caching",
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_xml",
            "--reasoning-parser qwen3",
        ],
    },
    "Qwen3-VL-30B-A3B-Instruct-FP8": {
        "hf_model":    "Qwen/Qwen3-VL-30B-A3B-Instruct",
        "description": "Qwen3-VL 30B MoE FP8 — vision-language model",
        "container":   "spark-vllm:Version_1",
        "tp": 1, "gpu_mem": 0.60, "max_len": 32768,
        "env": {},
        "extras": [
            "--trust-remote-code",
            "--kv-cache-dtype fp8",
            "--load-format fastsafetensors",
            "--enable-prefix-caching",
            "--limit-mm-per-prompt '{\"image\": 2}'",
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_coder",
        ],
    },
    "Qwen3-Omni-30B-A3B-Instruct": {
        "hf_model":    "Qwen/Qwen3-Omni-30B-A3B-Instruct",
        "description": "Qwen3-Omni 30B MoE — audio + vision + text multimodal",
        "container":   "vllm-node:Version_1",
        "tp": 1, "gpu_mem": 0.75, "max_len": 32768,
        "env": {},
        "extras": [
            "--trust-remote-code",
            "--load-format fastsafetensors",
            "--enable-prefix-caching",
            "--limit-mm-per-prompt '{\"image\": 2, \"audio\": 2}'",
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_coder",
        ],
    },
    "Qwen3-Coder-Next-FP8-Dynamic": {
        "hf_model":    "Qwen/Qwen3-Coder-480B-A22B-FP8-Dynamic",
        "description": "Qwen3-Coder-Next 480B MoE FP8-Dynamic — coding specialist",
        "container":   "vllm-node-tf5:latest",
        "tp": 1, "gpu_mem": 0.75, "max_len": 32768,
        "env": {},
        "extras": [
            "--kv-cache-dtype fp8",
            "--load-format fastsafetensors",
            "--attention-backend flashinfer",
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_coder",
        ],
    },
    "Qwen3-Coder-Next-int4-AutoRound": {
        "hf_model":    "Qwen/Qwen3-Coder-480B-A22B-Instruct",
        "description": "Qwen3-Coder-Next 480B MoE INT4 AutoRound — coding + throughput optimized",
        "container":   "vllm-node-tf5:latest",
        "tp": 1, "gpu_mem": 0.6, "max_len": 32768,
        "env": {
            "VLLM_MARLIN_USE_ATOMIC_ADD": "1",
            "VLLM_ALLOW_LONG_MAX_MODEL_LEN": "1",
            "VLLM_USE_FLASHINFER_MOE_FP8": "1",
        },
        "extras": [
            "--language-model-only",
            "--enable-chunked-prefill",
            "--max-num-batched-tokens 49152",
            "--max-num-seqs 384",
            "--kv-cache-dtype fp8",
            "--load-format fastsafetensors",
            "--optimization-level 3",
            "--performance-mode throughput",
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_coder",
        ],
    },
    "Nemotron-3-Nano-4B-FP8": {
        "hf_model":    "nvidia/Nemotron-3-Nano-4B-Instruct",
        "description": "Nemotron-3-Nano 4B FP8 — ultra-fast orchestrator / routing model",
        "container":   "spark-vllm:Version_1",
        "tp": 1, "gpu_mem": 0.5, "max_len": 8192,
        "env": {},
        "extras": [
            "--kv-cache-dtype fp8",
            "--enforce-eager",
            "--trust-remote-code",
            "--load-format fastsafetensors",
            "--enable-prefix-caching",
            "--tool-call-parser qwen3_coder",
            "--reasoning-parser nemotron_v3",
            "--enable-auto-tool-choice",
        ],
    },
    "Nemotron-3-Nano-30B-A3B-NVFP4": {
        "hf_model":    "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-NVFP4",
        "description": "Nemotron-3-Nano 30B NVFP4 — Blackwell-native MoE with nano_v3 reasoning",
        "container":   "vllm-node:Version_1",
        "tp": 1, "gpu_mem": 0.65, "max_len": 131072,
        "env": {
            "VLLM_USE_FLASHINFER_MOE_FP4": "1",
            "VLLM_FLASHINFER_MOE_BACKEND": "throughput",
        },
        "extras": [
            "--kv-cache-dtype fp8",
            "--enforce-eager",
            "--trust-remote-code",
            "--quantization modelopt_fp4",
            "--enable-auto-tool-choice",
            "--tool-call-parser qwen3_coder",
            "--reasoning-parser nano_v3",
        ],
    },
    "Nemotron-3-Super-120B-A12B-NVFP4": {
        "hf_model":    "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
        "description": "Nemotron-3-Super 120B NVFP4 — large reasoning model with CUTLASS MoE",
        "container":   "spark-vllm:Version_1",
        "tp": 1, "gpu_mem": 0.7, "max_len": 65536,
        "env": {
            "VLLM_FLASHINFER_ALLREDUCE_BACKEND": "trtllm",
            "VLLM_ALLOW_LONG_MAX_MODEL_LEN": "1",
        },
        "extras": [
            "--kv-cache-dtype fp8",
            "--moe-backend cutlass",
            "--trust-remote-code",
            "--enable-prefix-caching",
            "--load-format fastsafetensors",
            "--tool-call-parser qwen3_coder",
            "--enable-auto-tool-choice",
            "--reasoning-parser nemotron_v3",
        ],
    },
    "GPT-OSS-120B": {
        "hf_model":    "openai/gpt-oss-120b",
        "description": "OpenAI GPT-OSS 120B MXFP4 — open-weights GPT model",
        "container":   "vllm-node-mxfp4",
        "tp": 1, "gpu_mem": 0.7, "max_len": 65536,
        "env": {"VLLM_USE_FLASHINFER_MOE_MXFP4_MXFP8": "1"},
        "extras": [
            "--quantization mxfp4",
            "--kv-cache-dtype fp8",
            "--max-num-batched-tokens 8192",
            "--enable-prefix-caching",
            "--load-format fastsafetensors",
            "--tool-call-parser openai",
            "--reasoning-parser openai_gptoss",
            "--enable-auto-tool-choice",
        ],
    },
    "Mistral-Small-24B-Instruct-2501": {
        "hf_model":    "mistralai/Mistral-Small-24B-Instruct-2501",
        "description": "Mistral Small 24B — fast roleplay and instruction following",
        "container":   "vllm-node:Version_1",
        "tp": 1, "gpu_mem": 0.7, "max_len": 32768,
        "env": {},
        "extras": [
            "--trust-remote-code",
            "--enforce-eager",
            "--enable-auto-tool-choice",
            "--tool-call-parser mistral",
        ],
    },
    "Qwen3.5-35B-A3B-Uncensored-HauhauCS-Aggressive-Q4_K_M-GGUF": {
        "hf_model":    "HauhauCS/Qwen3.5-35B-A3B-Uncensored-Aggressive-GGUF",
        "description": "Qwen3.5 35B MoE Q4_K_M GGUF — uncensored, llama.cpp serving",
        "container":   "ghcr.io/martin-b78/llama-cpp-spark:latest",
        "tp": 1, "gpu_mem": 0.0, "max_len": 16384,
        "env": {"GGML_CUDA_ENABLE_UNIFIED_MEMORY": "1"},
        "extras": [
            "--ctx-size 16384",
            "--n-gpu-layers 99",
            "--parallel 4",
            "--no-mmap",
        ],
        "runtime": "llama-cpp",
    },
}

r = RECIPES.get(model_name)
if not r:
    # Unknown model — generate a minimal placeholder recipe
    r = {
        "hf_model":    f"<TODO: HuggingFace model ID for {model_name}>",
        "description": model_name,
        "container":   "<TODO: container image>",
        "tp": 1, "gpu_mem": 0.7, "max_len": 32768,
        "env": {}, "extras": [],
    }

runtime  = r.get("runtime", "vllm")
hf_model = r["hf_model"]
extras   = "\n".join(f"    {e} \\" for e in r["extras"])
env_block = ""
if r["env"]:
    env_lines = "\n".join(f"  {k}: '{v}'" for k, v in r["env"].items())
    env_block = f"env:\n{env_lines}\n"

if runtime == "llama-cpp":
    cmd = (
        f"llama-server \\\n"
        f"    -hf {hf_model} \\\n"
        f"    --host {{host}} --port {{port}} \\\n"
        + "\n".join(f"    {e} \\" for e in r["extras"])
        + "\n"
    )
else:
    cmd = (
        f"vllm serve {hf_model} \\\n"
        f"    --served-model-name {model_name} \\\n"
        f"    --host {{host}} --port {{port}} \\\n"
        f"    --tensor-parallel-size {{tensor_parallel}} \\\n"
        f"    --gpu-memory-utilization {{gpu_memory_utilization}} \\\n"
        f"    --max-model-len {{max_model_len}} \\\n"
        + "\n".join(f"    {e} \\" for e in r["extras"])
        + "\n"
    )

yaml = f"""recipe_version: '1'
name: {model_name}
description: {r['description']}
model: {hf_model}
cluster_only: false
container: {r['container']}
defaults:
  port: 8000
  host: 0.0.0.0
  tensor_parallel: {r['tp']}
  gpu_memory_utilization: {r['gpu_mem']}
  max_model_len: {r['max_len']}
{env_block}command: |
  {cmd.rstrip()}
solo_only: false
"""

with open(out_path, "w") as f:
    f.write(yaml)

print(f"  Recipe YAML: {out_path}")
PYEOF

    if [[ $? -ne 0 ]]; then
        log "  ${YELLOW}Warning: recipe.yaml generation failed for $model${NC}"
    fi
}

# ---------------------------------------------------------------------------
# Crash-resume checkpoint functions
# ---------------------------------------------------------------------------

# Create a new checkpoint file for this session and register it as last-known
init_checkpoint() {
    mkdir -p "$CHECKPOINT_DIR"
    CHECKPOINT_FILE="$CHECKPOINT_DIR/session_${TIMESTAMP}.json"
    python3 - "$CHECKPOINT_FILE" "$MODE" "$PP" "$TG" "$DEPTH" "$RUNS" <<'PYEOF'
import json, sys, datetime
f, mode, pp, tg, depth, runs = sys.argv[1:]
data = {
    "session_id": f.split("_")[-1].replace(".json", ""),
    "started_at": datetime.datetime.now().isoformat(),
    "mode": mode,
    "settings": {"pp": pp, "tg": tg, "depth": depth, "runs": int(runs)},
    "models": [],
}
with open(f, "w") as fh:
    json.dump(data, fh, indent=2)
PYEOF
    echo "$CHECKPOINT_FILE" > "$LAST_SESSION_FILE"
}

# Mark a model as started (written BEFORE warmup so a crash is detectable)
checkpoint_model_start() {
    local model="$1"
    [[ -z "$CHECKPOINT_FILE" ]] && return
    python3 - "$CHECKPOINT_FILE" "$model" <<'PYEOF'
import json, sys, datetime
f, model = sys.argv[1:]
with open(f) as fh:
    data = json.load(fh)
data["models"].append({
    "model": model,
    "status": "started",
    "started_at": datetime.datetime.now().isoformat(),
})
with open(f, "w") as fh:
    json.dump(data, fh, indent=2)
PYEOF
}

# Mark a model as completed with its result (OK / FAIL / INCOHERENT / EMPTY)
checkpoint_model_done() {
    local model="$1"
    local result="$2"
    [[ -z "$CHECKPOINT_FILE" ]] && return
    python3 - "$CHECKPOINT_FILE" "$model" "$result" <<'PYEOF'
import json, sys, datetime
f, model, result = sys.argv[1:]
with open(f) as fh:
    data = json.load(fh)
for entry in reversed(data["models"]):
    if entry["model"] == model and entry.get("status") == "started":
        entry["status"] = "completed"
        entry["result"] = result
        entry["completed_at"] = datetime.datetime.now().isoformat()
        break
with open(f, "w") as fh:
    json.dump(data, fh, indent=2)
PYEOF
}

# Load a previous checkpoint and populate SKIP_MODELS (models already done)
load_resume_checkpoint() {
    if [[ ! -f "$LAST_SESSION_FILE" ]]; then
        echo -e "${RED}Error: no previous session found. Run without --resume first.${NC}" >&2
        exit 1
    fi
    local prev_cp
    prev_cp=$(cat "$LAST_SESSION_FILE")
    if [[ ! -f "$prev_cp" ]]; then
        echo -e "${RED}Error: checkpoint file not found: $prev_cp${NC}" >&2
        exit 1
    fi

    log "  ${CYAN}Resuming from checkpoint:${NC} $prev_cp"

    local info
    info=$(python3 - "$prev_cp" <<'PYEOF'
import json, sys
data = json.load(open(sys.argv[1]))
completed = [e["model"] for e in data.get("models", []) if e.get("status") == "completed"]
last_started = next(
    (e["model"] for e in reversed(data.get("models", [])) if e.get("status") == "started"),
    "",
)
s = data.get("settings", {})
print("MODE:" + data.get("mode", ""))
print("SETTINGS:pp=" + str(s.get("pp","")) + "  tg=" + str(s.get("tg","")) +
      "  depth=" + str(s.get("depth","")) + "  runs=" + str(s.get("runs","")))
print("LAST_STARTED:" + last_started)
for m in completed:
    print("DONE:" + m)
PYEOF
)
    while IFS= read -r line; do
        case "$line" in
            DONE:*)    SKIP_MODELS+=("${line#DONE:}") ;;
            MODE:*)    log "  Previous mode     : ${line#MODE:}" ;;
            SETTINGS:*) log "  Previous settings : ${line#SETTINGS:}" ;;
            LAST_STARTED:*)
                local last="${line#LAST_STARTED:}"
                if [[ -n "$last" ]]; then
                    log "  ${YELLOW}Crashed while running: $last${NC} — will re-run it."
                fi
                ;;
        esac
    done <<< "$info"

    log "  Skipping ${#SKIP_MODELS[@]} already-completed model(s)."
    log ""
}

# --------------- MAIN ---------------
log ""
log "${BOLD}============================================================${NC}"
log "${BOLD}  DGX Spark Model Benchmark${NC}"
log "${BOLD}  powered by llama-benchy (github.com/eugr/llama-benchy)${NC}"
log "${BOLD}============================================================${NC}"
log ""
log "  Endpoint : $LLAMA_SWAP_URL"
log "  Mode     : $MODE"
log "  Settings : pp=$PP  tg=$TG  depth=$DEPTH  runs=$RUNS"
log "  Date     : $(date '+%Y-%m-%d %H:%M')"
log "  Results  : $RESULTS_DIR"
log ""

# Mode descriptions
case "$MODE" in
    medium-log)
        log "  ${CYAN}Profile: Medium Log Baseline${NC}"
        log "  ${DIM}Simulates a ~50-page document (depth=16384). Establishes your${NC}"
        log "  ${DIM}baseline generation speed with a moderately full KV cache.${NC}"
        ;;
    stress)
        log "  ${CYAN}Profile: Massive Log Stress Test${NC}"
        log "  ${DIM}Doubles context to simulate a massive error log (depth=32768).${NC}"
        log "  ${DIM}Watch for tg tok/s drop — that's the unified memory bottleneck.${NC}"
        ;;
    extreme)
        log "  ${CYAN}Profile: Extreme Limit Test${NC}"
        log "  ${DIM}Pushes to ~200 pages (depth=65535). Tests if the system can${NC}"
        log "  ${DIM}process it without crashing or heavy swap paging.${NC}"
        ;;
    quick)
        log "  ${DIM}Quick smoke test — just checking if models respond.${NC}"
        ;;
    full)
        log "  ${DIM}Full comprehensive sweep — broad pp/tg/depth combinations.${NC}"
        ;;
    arena)
        log "  ${CYAN}Profile: Spark-Arena Leaderboard Submission${NC}"
        log "  ${DIM}Official spark-arena.com benchmark spec: 7 depth levels × 4 concurrency${NC}"
        log "  ${DIM}levels × 3 runs = 84 data points per model.${NC}"
        log "  ${DIM}Generates results.csv + recipe.yaml per model in:${NC}"
        log "  ${DIM}  $ARENA_DIR${NC}"
        ;;
esac
log ""
log "  ${DIM}pp = prompt processing (how fast the model reads your input)${NC}"
log "  ${DIM}tg = token generation  (how fast the model writes its reply)${NC}"
log "  ${DIM}depth = pre-filled context tokens (simulates document size)${NC}"
log ""

# Initialize crash-resume checkpoint and, if --resume, load previous state
init_checkpoint
[[ "$RESUME" == true ]] && load_resume_checkpoint

# Detect how llama-benchy is available: uvx (preferred), direct, or missing
if uvx llama-benchy --help > /dev/null 2>&1; then
    BENCHY_CMD="uvx llama-benchy"
elif command -v llama-benchy > /dev/null 2>&1; then
    BENCHY_CMD="llama-benchy"
else
    log "${RED}Error: llama-benchy not found.${NC}"
    log ""
    log "Install one of these ways:"
    log "  ${BOLD}uvx${NC} (no install needed):  works if 'uv' is installed"
    log "  ${BOLD}pip install llama-benchy${NC}  (install into current venv/system)"
    log "  ${BOLD}uv pip install llama-benchy${NC}"
    exit 1
fi
log "  ${DIM}Using: $BENCHY_CMD${NC}"

# Fetch model list
MODELS=$(curl -sf "$LLAMA_SWAP_URL/v1/models" | jq -r '.data[].id' | sort)
MODEL_COUNT=$(echo "$MODELS" | wc -l)

# Apply filters
if [[ ${#FILTERS[@]} -gt 0 ]]; then
    log "Filtering models matching: ${FILTERS[*]}"
    FILTERED=""
    for m in $MODELS; do
        for f in "${FILTERS[@]}"; do
            if [[ "$m" == *"$f"* ]]; then
                FILTERED="${FILTERED}${m}\n"
            fi
        done
    done
    MODELS=$(echo -e "$FILTERED" | grep -v '^$' | sort -u)
    MODEL_COUNT=$(echo "$MODELS" | wc -l)
fi

log "Found ${BOLD}${MODEL_COUNT}${NC} model(s) to benchmark."
log ""

PASS=0
FAIL=0
IDX=0
TOTAL_START=$(date +%s.%N)

# Collect results for final summary
declare -A SUMMARY_PP SUMMARY_TG SUMMARY_PEAK SUMMARY_TTFT SUMMARY_STATUS SUMMARY_DEGRADATION

for MODEL in $MODELS; do
    IDX=$((IDX + 1))

    # Skip models already completed in a --resume session
    if [[ "$RESUME" == true ]]; then
        _skip=false
        for _done in "${SKIP_MODELS[@]:-}"; do
            [[ "$MODEL" == "$_done" ]] && { _skip=true; break; }
        done
        if [[ "$_skip" == true ]]; then
            log "  ${DIM}⏭  [$IDX/$MODEL_COUNT] $MODEL — skipped (completed in previous session)${NC}"
            continue
        fi
    fi

    log "${BOLD}============================================================${NC}"
    log "${BOLD}  [$IDX/$MODEL_COUNT] $MODEL${NC}"
    log "${BOLD}============================================================${NC}"
    log ""

    # Write checkpoint BEFORE starting so a crash is detectable
    checkpoint_model_start "$MODEL"

    # Unload previous model to get a clean measurement
    unload_all

    # Load this model
    WARMUP_FAIL_REASON=""
    if ! warmup_model "$MODEL"; then
        FAIL=$((FAIL + 1))
        SUMMARY_STATUS[$MODEL]="${WARMUP_FAIL_REASON:-FAIL}"
        checkpoint_model_done "$MODEL" "${WARMUP_FAIL_REASON:-FAIL}"
        log ""
        continue
    fi

    # Benchmark it
    if run_benchy "$MODEL"; then
        PASS=$((PASS + 1))
        SUMMARY_STATUS[$MODEL]="OK"
        checkpoint_model_done "$MODEL" "OK"
        # In arena mode: generate recipe YAML and check vs personal best
        if [[ "$MODE" == "arena" ]]; then
            generate_recipe_yaml "$MODEL" 2>&1 | tee -a "$REPORT_FILE"
            local arena_json="$RESULTS_DIR/${MODEL//\//_}_${TIMESTAMP}.json"
            local arena_recipe="$ARENA_DIR/${MODEL//\//_}/recipe.yaml"
            check_and_suggest_submit "$MODEL" "$arena_json" "$arena_recipe" 2>&1 | tee -a "$REPORT_FILE"
        fi

        # Extract numbers for final summary from JSON
        local_json="$RESULTS_DIR/${MODEL//\//_}_${TIMESTAMP}.json"
        if [[ -f "$local_json" ]]; then
            # Extract metrics using jq for the summary table
            # We look for depth 0 (baseline) and the deepest result
            baseline_tg=$(jq -r '.benchmarks[] | select(.context_size == 0) | .tg_throughput.mean // empty' "$local_json" | head -n1)
            max_depth=$(jq -r '.benchmarks[].context_size' "$local_json" | sort -rn | head -n1)
            deepest_tg=$(jq -r ".benchmarks[] | select(.context_size == $max_depth) | .tg_throughput.mean // empty" "$local_json" | head -n1)
            
            # Baseline metrics for the summary table
            SUMMARY_PP[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | if .pp_throughput.std > 0.5 then "\(.pp_throughput.mean + 0.5 | floor) +/-\(.pp_throughput.std + 0.5 | floor)" else "\(.pp_throughput.mean + 0.5 | floor)" end' "$local_json" | head -n1)
            SUMMARY_TG[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | if .tg_throughput.std > 0.5 then "\((.tg_throughput.mean * 10 + 0.5 | floor) / 10) +/-\((.tg_throughput.std * 10 + 0.5 | floor) / 10)" else "\((.tg_throughput.mean * 10 + 0.5 | floor) / 10)" end' "$local_json" | head -n1)
            SUMMARY_PEAK[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | .peak_throughput.mean + 0.5 | floor' "$local_json" | head -n1)
            SUMMARY_TTFT[$MODEL]=$(jq -r '.benchmarks[] | select(.context_size == 0) | .e2e_ttft.mean + 0.5 | floor' "$local_json" | head -n1)
            
            if [[ -n "$baseline_tg" && -n "$deepest_tg" && "$max_depth" -gt 0 ]]; then
                pct=$(echo "scale=1; (($deepest_tg - $baseline_tg) / $baseline_tg) * 100" | bc)
                SUMMARY_DEGRADATION[$MODEL]="${pct}% @$((max_depth/1024))k"
            fi
        fi
    else
        FAIL=$((FAIL + 1))
        SUMMARY_STATUS[$MODEL]="FAIL"
        checkpoint_model_done "$MODEL" "FAIL"
    fi
    log ""
done

# Final cleanup
unload_all

TOTAL_END=$(date +%s.%N)
TOTAL_ELAPSED=$(echo "$TOTAL_END - $TOTAL_START" | bc)
TOTAL_MIN=$(echo "scale=1; $TOTAL_ELAPSED / 60" | bc)

# =============================================
# FINAL REPORT
# =============================================
log ""
log "${BOLD}============================================================${NC}"
log "${BOLD}  BENCHMARK REPORT — DGX Spark${NC}"
log "${BOLD}============================================================${NC}"
log ""
log "  Date: $(date '+%Y-%m-%d %H:%M')  |  Mode: $MODE  |  Runs: $RUNS"
log "  Depths tested: $DEPTH"
log "  Models tested: $MODEL_COUNT  |  Passed: ${GREEN}$PASS${NC}  Failed: ${RED}$FAIL${NC}"
log "  Total benchmark time: ${TOTAL_MIN} min"
log "  Checkpoint : $CHECKPOINT_FILE"
[[ $FAIL -gt 0 ]] && log "  ${YELLOW}Tip: if this was interrupted, resume with: ./benchmark-models.sh --resume${NC}"
log ""
log "  ${BOLD}$(printf '%-42s  %14s  %12s  %8s  %8s  %14s' 'Model' 'Read (pp)' 'Write (tg)' 'Peak' 'TTFT' 'Deep ctx')${NC}"
log "  ${DIM}$(printf '%-42s  %14s  %12s  %8s  %8s  %14s' '' 'tok/s' 'tok/s' 'tok/s' 'ms' 'degradation')${NC}"
log "  $(printf '%.0s-' {1..106})"

for MODEL in $MODELS; do
    local_name="$MODEL"
    [[ ${#local_name} -gt 42 ]] && local_name="${local_name:0:39}..."

    status="${SUMMARY_STATUS[$MODEL]:-FAIL}"
    pp="${SUMMARY_PP[$MODEL]:-—}"
    tg="${SUMMARY_TG[$MODEL]:-—}"
    peak="${SUMMARY_PEAK[$MODEL]:-—}"
    ttft="${SUMMARY_TTFT[$MODEL]:-—}"
    degrad="${SUMMARY_DEGRADATION[$MODEL]:-—}"

    if [[ "$status" == "OK" ]]; then
        printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "$pp" "$tg" "$peak" "$ttft" "$degrad"
        log "${GREEN}${line}${NC}"
    elif [[ "$status" == "INCOHERENT" ]]; then
        printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "INCOHERENT" "—" "—" "—" "—"
        log "${YELLOW}${line}${NC}"
    else
        printf -v line "  %-42s  %14s  %12s  %8s  %8s  %14s" "$local_name" "FAIL" "—" "—" "—" "—"
        log "${RED}${line}${NC}"
    fi
done

log ""
log "  ${DIM}---------------------------------------------------------------${NC}"
log "  ${DIM}How to read this table:${NC}"
log "  ${DIM}${NC}"
log "  ${DIM}  Read (pp)    = How fast the model reads your prompt (higher = better).${NC}"
log "  ${DIM}  Write (tg)   = How fast the model types its answer at depth=0 baseline.${NC}"
log "  ${DIM}                  This is the speed you feel when chatting.${NC}"
log "  ${DIM}                  Humans read at ~4 tok/s, so 20+ feels smooth.${NC}"
log "  ${DIM}  Peak         = Fastest burst speed observed in a 1-second window.${NC}"
log "  ${DIM}  TTFT         = Time until the first word appears (lower = better).${NC}"
log "  ${DIM}  Deep ctx     = Speed change at max tested depth vs baseline.${NC}"
log "  ${DIM}                  >-15% = unified memory bandwidth bottleneck.${NC}"
log "  ${DIM}---------------------------------------------------------------${NC}"
log ""
log "  Results saved to:"
log "    Report     : $REPORT_FILE"
log "    JSON data  : $RESULTS_DIR/*_${TIMESTAMP}.json"
log "    Forum tables: $RESULTS_DIR/*_${TIMESTAMP}.md"
log ""
log "  ${DIM}Tip: To share on NVIDIA forums, copy the llama-benchy tables${NC}"
log "  ${DIM}from the .md files — they use the standard format everyone knows.${NC}"

if [[ "$MODE" == "arena" && -n "$ARENA_DIR" ]]; then
    log ""
    log "${BOLD}============================================================${NC}"
    log "${BOLD}  SPARK-ARENA SUBMISSION FILES${NC}"
    log "${BOLD}============================================================${NC}"
    log ""
    log "  Submission directory: ${CYAN}$ARENA_DIR${NC}"
    log ""
    log "  Per-model folders (one submission per model):"
    for MODEL in $MODELS; do
        safe="${MODEL//\//_}"
        model_dir="$ARENA_DIR/$safe"
        if [[ -f "$model_dir/results.csv" && -f "$model_dir/recipe.yaml" ]]; then
            log "    ${GREEN}✓${NC} $MODEL"
            log "        recipe.yaml : $model_dir/recipe.yaml"
            log "        results.csv : $model_dir/results.csv"
        elif [[ "${SUMMARY_STATUS[$MODEL]:-FAIL}" != "OK" ]]; then
            log "    ${RED}✗${NC} $MODEL  (benchmark failed — no submission files)"
        fi
    done
    log ""
    log "  ${BOLD}How to submit to spark-arena.com/admin:${NC}"
    log "  ${DIM}1. Open https://spark-arena.com/admin${NC}"
    log "  ${DIM}2. For each model folder above:${NC}"
    log "  ${DIM}     a. Paste or upload the contents of recipe.yaml${NC}"
    log "  ${DIM}     b. Upload results.csv${NC}"
    log "  ${DIM}3. Submit one entry per model.${NC}"
    log ""
    log "  ${DIM}Note: The recipe.yaml 'model:' field uses the canonical HuggingFace ID.${NC}"
    log "  ${DIM}If your model was downloaded from a different source, update it.${NC}"
fi
log "${BOLD}============================================================${NC}"

Ok I did the first testrun - took about 8h here is the result. Will upload the files to https://spark-arena.com


e[1m============================================================e[0m
e[1m  DGX Spark Model Benchmarke[0m
e[1m  powered by llama-benchy (github.com/eugr/llama-benchy)e[0m
e[1m============================================================e[0m

  Endpoint : http://localhost:28080
  Mode     : arena
  Settings : pp=2048  tg=128  depth=0 4096 8192 16384 32768 65535 100000  runs=3
  Date     : 2026-04-20 18:38
  Results  : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks

  e[0;36mProfile: Spark-Arena Leaderboard Submissione[0m
  e[2mOfficial spark-arena.com benchmark spec: 7 depth levels × 4 concurrencye[0m
  e[2mlevels × 3 runs = 84 data points per model.e[0m
  e[2mGenerates results.csv + recipe.yaml per model in:e[0m
  e[2m  /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/arena-submission/20260420_183825e[0m

  e[2mpp = prompt processing (how fast the model reads your input)e[0m
  e[2mtg = token generation  (how fast the model writes its reply)e[0m
  e[2mdepth = pre-filled context tokens (simulates document size)e[0m

Filtering models matching: GPT-OSS-120B
Found e[1m1e[0m model(s) to benchmark.

e[1m============================================================e[0m
e[1m  [1/1] GPT-OSS-120Be[0m
e[1m============================================================e[0m

  Loading model via llama-swap...
  e[0;36mCoherence check:e[0m "The user asks: "Write a short python hello world script." Simple answer: Provide a small Python script that prints "Hello, World!". Possibly include a"
  Model ready (loaded in 1.600343363s)
  Profile: Spark-Arena leaderboard (7 depths × 4 concurrency levels)
  Running llama-benchy (pp=2048  tg=128  depth=0 4096 8192 16384 32768 65535 100000  runs=3  concurrency=1 2 5 10)...

  e[0;36mSaving arena submission CSV...e[0m
  e[2mArena CSV  : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/arena-submission/20260420_183825/GPT-OSS-120B/results.csve[0m
  e[0;36mllama-benchy results (copy this to share on forums):e[0m

| model        |                   test |      t/s (total) |         t/s (req) |      peak t/s |   peak t/s (req) |             ttfr (ms) |          est_ppt (ms) |         e2e_ttft (ms) |
|:-------------|-----------------------:|-----------------:|------------------:|--------------:|-----------------:|----------------------:|----------------------:|----------------------:|
| GPT-OSS-120B |            pp2048 (c1) | 3947.98 ± 181.15 |  3947.98 ± 181.15 |               |                  |        531.89 ± 19.05 |        455.64 ± 19.05 |        634.40 ± 21.92 |
| GPT-OSS-120B |             tg128 (c1) |     30.40 ± 0.14 |      30.40 ± 0.14 |  31.33 ± 0.47 |     31.33 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |            pp2048 (c2) |  3101.86 ± 47.23 | 2927.56 ± 1067.90 |               |                  |       785.69 ± 261.70 |       709.44 ± 261.70 |       1141.88 ± 19.79 |
| GPT-OSS-120B |             tg128 (c2) |     53.38 ± 0.57 |      26.89 ± 0.28 |  56.33 ± 0.47 |     28.17 ± 0.37 |                       |                       |                       |
| GPT-OSS-120B |            pp2048 (c5) | 4058.86 ± 254.53 | 1385.24 ± 1038.82 |               |                  |      1863.99 ± 618.78 |      1787.74 ± 618.78 |      2284.64 ± 158.14 |
| GPT-OSS-120B |             tg128 (c5) |     85.82 ± 0.93 |      17.27 ± 0.18 | 100.33 ± 4.11 |     20.07 ± 0.85 |                       |                       |                       |
| GPT-OSS-120B |           pp2048 (c10) |  4570.57 ± 10.53 |  982.42 ± 1014.99 |               |                  |     2856.45 ± 1077.50 |     2780.20 ± 1077.50 |       3953.18 ± 65.14 |
| GPT-OSS-120B |            tg128 (c10) |    126.44 ± 3.49 |      12.79 ± 0.36 | 153.33 ± 2.36 |     15.33 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |    ctx_pp @ d4096 (c1) |   4253.76 ± 6.71 |    4253.76 ± 6.71 |               |                  |        921.66 ± 26.31 |        845.41 ± 26.31 |       1027.30 ± 25.76 |
| GPT-OSS-120B |    ctx_tg @ d4096 (c1) |     29.27 ± 1.08 |      29.27 ± 1.08 |  30.67 ± 0.47 |     30.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |    pp2048 @ d4096 (c1) |  3723.75 ± 82.19 |   3723.75 ± 82.19 |               |                  |        626.50 ± 12.06 |        550.25 ± 12.06 |        731.18 ± 12.20 |
| GPT-OSS-120B |     tg128 @ d4096 (c1) |     29.61 ± 0.14 |      29.61 ± 0.14 |  30.33 ± 0.47 |     30.33 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |    ctx_pp @ d4096 (c2) | 3926.50 ± 223.05 |  2923.58 ± 974.45 |               |                  |      1440.51 ± 381.63 |      1364.26 ± 381.63 |       1837.70 ± 97.13 |
| GPT-OSS-120B |    ctx_tg @ d4096 (c2) |     50.95 ± 2.56 |      25.61 ± 1.42 |  55.33 ± 0.94 |     27.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |    pp2048 @ d4096 (c2) |  2954.37 ± 68.92 |  2673.38 ± 969.88 |               |                  |       958.21 ± 319.99 |       881.96 ± 319.99 |       1370.17 ± 37.20 |
| GPT-OSS-120B |     tg128 @ d4096 (c2) |     52.40 ± 0.40 |      26.39 ± 0.20 |  55.33 ± 0.94 |     27.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |    ctx_pp @ d4096 (c5) | 3959.01 ± 171.25 |  1530.72 ± 948.16 |               |                  |     3122.99 ± 1215.13 |     3046.74 ± 1215.13 |      4519.15 ± 199.15 |
| GPT-OSS-120B |    ctx_tg @ d4096 (c5) |     82.88 ± 0.91 |      16.78 ± 0.18 |  94.33 ± 0.94 |     18.87 ± 0.34 |                       |                       |                       |
| GPT-OSS-120B |    pp2048 @ d4096 (c5) | 3508.96 ± 310.49 |  1345.96 ± 929.56 |               |                  |      2180.92 ± 901.07 |      2104.67 ± 901.07 |      2923.07 ± 269.42 |
| GPT-OSS-120B |     tg128 @ d4096 (c5) |     81.88 ± 0.99 |      16.51 ± 0.22 |  94.00 ± 4.32 |     18.80 ± 0.91 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d4096 (c10) |  4219.29 ± 90.49 |  1055.24 ± 967.79 |               |                  |     5357.62 ± 2501.59 |     5281.37 ± 2501.59 |      8267.47 ± 589.06 |
| GPT-OSS-120B |   ctx_tg @ d4096 (c10) |    101.29 ± 7.74 |      11.83 ± 0.70 | 136.67 ± 4.71 |     13.97 ± 0.18 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d4096 (c10) |  3872.15 ± 99.58 |   876.29 ± 877.93 |               |                  |     3627.64 ± 1456.42 |     3551.39 ± 1456.42 |      5227.85 ± 119.70 |
| GPT-OSS-120B |    tg128 @ d4096 (c10) |    113.68 ± 1.25 |      11.54 ± 0.18 | 140.00 ± 0.00 |     14.03 ± 0.18 |                       |                       |                       |
| GPT-OSS-120B |    ctx_pp @ d8192 (c1) |  4119.74 ± 22.64 |   4119.74 ± 22.64 |               |                  |       1854.50 ± 16.28 |       1778.25 ± 16.28 |       1963.30 ± 11.53 |
| GPT-OSS-120B |    ctx_tg @ d8192 (c1) |     28.67 ± 0.52 |      28.67 ± 0.52 |  30.00 ± 0.00 |     30.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |    pp2048 @ d8192 (c1) |  2914.02 ± 95.84 |   2914.02 ± 95.84 |               |                  |        779.81 ± 22.82 |        703.56 ± 22.82 |        886.68 ± 25.42 |
| GPT-OSS-120B |     tg128 @ d8192 (c1) |     28.66 ± 0.39 |      28.66 ± 0.39 |  30.00 ± 0.00 |     30.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |    ctx_pp @ d8192 (c2) | 3721.07 ± 180.93 |  2919.03 ± 988.26 |               |                  |      2876.38 ± 942.55 |      2800.13 ± 942.55 |      3888.80 ± 190.13 |
| GPT-OSS-120B |    ctx_tg @ d8192 (c2) |     50.34 ± 0.82 |      25.35 ± 0.41 |  54.00 ± 0.00 |     27.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |    pp2048 @ d8192 (c2) | 2703.94 ± 229.07 |  2055.30 ± 668.13 |               |                  |      1170.32 ± 303.39 |      1094.07 ± 303.39 |      1512.09 ± 113.92 |
| GPT-OSS-120B |     tg128 @ d8192 (c2) |     50.81 ± 0.15 |      25.54 ± 0.02 |  54.00 ± 0.00 |     27.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |    ctx_pp @ d8192 (c5) |  3817.51 ± 61.91 | 1775.68 ± 1200.80 |               |                  |     5824.30 ± 2715.35 |     5748.05 ± 2715.35 |      9135.03 ± 562.33 |
| GPT-OSS-120B |    ctx_tg @ d8192 (c5) |     67.24 ± 0.44 |      15.41 ± 0.87 |  85.00 ± 0.00 |     17.13 ± 0.34 |                       |                       |                       |
| GPT-OSS-120B |    pp2048 @ d8192 (c5) | 2959.28 ± 255.91 |  1023.15 ± 637.72 |               |                  |      2649.12 ± 990.25 |      2572.87 ± 990.25 |      3466.69 ± 317.09 |
| GPT-OSS-120B |     tg128 @ d8192 (c5) |     79.18 ± 1.02 |      15.97 ± 0.19 |  92.67 ± 1.70 |     18.53 ± 0.50 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d8192 (c10) |  3900.45 ± 54.70 | 1128.22 ± 1080.85 |               |                  |    10850.66 ± 5637.60 |    10774.41 ± 5637.60 |    15268.65 ± 3789.88 |
| GPT-OSS-120B |   ctx_tg @ d8192 (c10) |     56.59 ± 0.92 |       9.14 ± 2.01 | 126.67 ± 4.71 |     12.93 ± 0.63 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d8192 (c10) |   3209.98 ± 9.62 |   757.23 ± 762.36 |               |                  |     4200.36 ± 1749.65 |     4124.10 ± 1749.65 |       6283.87 ± 61.18 |
| GPT-OSS-120B |    tg128 @ d8192 (c10) |    106.44 ± 1.48 |      10.81 ± 0.16 | 137.00 ± 3.56 |     13.77 ± 0.42 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d16384 (c1) | 3208.08 ± 149.31 |  3208.08 ± 149.31 |               |                  |      4606.14 ± 211.86 |      4529.89 ± 211.86 |      4716.57 ± 209.22 |
| GPT-OSS-120B |   ctx_tg @ d16384 (c1) |     27.83 ± 0.09 |      27.83 ± 0.09 |  28.67 ± 0.47 |     28.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d16384 (c1) | 2295.21 ± 113.84 |  2295.21 ± 113.84 |               |                  |        970.78 ± 45.13 |        894.53 ± 45.13 |       1085.98 ± 42.79 |
| GPT-OSS-120B |    tg128 @ d16384 (c1) |     27.94 ± 0.12 |      27.94 ± 0.12 |  28.67 ± 0.47 |     28.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d16384 (c2) |  3167.92 ± 38.67 |  2322.68 ± 694.85 |               |                  |     6923.30 ± 2079.76 |     6847.05 ± 2079.76 |      9087.10 ± 136.80 |
| GPT-OSS-120B |   ctx_tg @ d16384 (c2) |     45.89 ± 0.83 |      23.28 ± 0.42 |  50.67 ± 0.94 |     25.33 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d16384 (c2) | 1989.58 ± 131.27 |  1521.26 ± 576.35 |               |                  |      1597.90 ± 466.71 |      1521.65 ± 466.71 |      2054.69 ± 133.71 |
| GPT-OSS-120B |    tg128 @ d16384 (c2) |     47.64 ± 0.42 |      23.94 ± 0.28 |  50.00 ± 0.00 |     25.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d16384 (c5) |  3141.31 ± 33.66 |  1375.85 ± 876.51 |               |                  |    14368.87 ± 6468.58 |    14292.62 ± 6468.58 |    19893.51 ± 4133.10 |
| GPT-OSS-120B |   ctx_tg @ d16384 (c5) |     32.68 ± 0.82 |      11.83 ± 3.29 |  78.33 ± 2.36 |     16.07 ± 0.68 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d16384 (c5) |  2371.31 ± 42.45 |   850.51 ± 697.67 |               |                  |     3523.64 ± 1269.28 |     3447.39 ± 1269.28 |       4308.09 ± 80.52 |
| GPT-OSS-120B |    tg128 @ d16384 (c5) |     71.13 ± 0.94 |      14.32 ± 0.26 |  83.67 ± 1.89 |     16.73 ± 0.44 |                       |                       |                       |
| GPT-OSS-120B |  ctx_pp @ d16384 (c10) |  3136.56 ± 38.93 |   862.88 ± 760.58 |               |                  |   26198.68 ± 13026.42 |   26122.43 ± 13026.42 |   32921.75 ± 11570.45 |
| GPT-OSS-120B |  ctx_tg @ d16384 (c10) |     26.83 ± 0.40 |       5.96 ± 2.70 | 103.67 ± 4.50 |     11.93 ± 1.71 |                       |                       |                       |
| GPT-OSS-120B |  pp2048 @ d16384 (c10) |  2389.99 ± 33.59 |   574.34 ± 595.16 |               |                  |     5791.76 ± 2479.52 |     5715.51 ± 2479.52 |      8469.52 ± 134.09 |
| GPT-OSS-120B |   tg128 @ d16384 (c10) |     96.43 ± 1.35 |       9.88 ± 0.14 | 120.00 ± 0.00 |     12.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d32768 (c1) |  2265.54 ± 91.99 |   2265.54 ± 91.99 |               |                  |     12934.07 ± 558.65 |     12857.82 ± 558.65 |     13051.20 ± 561.44 |
| GPT-OSS-120B |   ctx_tg @ d32768 (c1) |     25.61 ± 0.43 |      25.61 ± 0.43 |  27.00 ± 0.00 |     27.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d32768 (c1) |  1541.87 ± 45.41 |   1541.87 ± 45.41 |               |                  |       1405.65 ± 38.84 |       1329.40 ± 38.84 |       1520.58 ± 41.31 |
| GPT-OSS-120B |    tg128 @ d32768 (c1) |     26.04 ± 0.11 |      26.04 ± 0.11 |  27.00 ± 0.00 |     27.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d32768 (c2) |  2307.66 ± 18.85 |  1670.49 ± 509.79 |               |                  |    19237.48 ± 5796.01 |    19161.23 ± 5796.01 |     24789.07 ± 452.69 |
| GPT-OSS-120B |   ctx_tg @ d32768 (c2) |     35.17 ± 1.60 |      19.15 ± 1.53 |  42.67 ± 0.94 |     21.83 ± 1.07 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d32768 (c2) | 1363.74 ± 127.63 |   931.47 ± 289.18 |               |                  |      2456.81 ± 604.13 |      2380.56 ± 604.13 |      3017.05 ± 294.17 |
| GPT-OSS-120B |    tg128 @ d32768 (c2) |     41.95 ± 0.17 |      21.07 ± 0.08 |  44.67 ± 0.94 |     22.33 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d32768 (c5) |  2309.29 ± 21.36 |  1008.59 ± 622.29 |               |                  |   38716.88 ± 17482.64 |   38640.63 ± 17482.64 |   47536.88 ± 14755.77 |
| GPT-OSS-120B |   ctx_tg @ d32768 (c5) |     12.65 ± 0.22 |       7.06 ± 4.10 |  65.00 ± 0.00 |     14.27 ± 1.57 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d32768 (c5) |   1554.37 ± 9.47 |   564.05 ± 351.33 |               |                  |     4821.69 ± 1958.83 |     4745.44 ± 1958.83 |       6549.27 ± 66.56 |
| GPT-OSS-120B |    tg128 @ d32768 (c5) |     63.59 ± 0.57 |      12.86 ± 0.10 |  74.00 ± 1.41 |     14.80 ± 0.40 |                       |                       |                       |
| GPT-OSS-120B |  ctx_pp @ d32768 (c10) |   2334.23 ± 6.74 |   664.93 ± 581.02 |               |                  |   68725.40 ± 35276.98 |   68649.15 ± 35276.98 |   78601.84 ± 33602.79 |
| GPT-OSS-120B |  ctx_tg @ d32768 (c10) |     10.54 ± 0.17 |       3.17 ± 2.34 |  84.00 ± 4.32 |     11.23 ± 2.97 |                       |                       |                       |
| GPT-OSS-120B |  pp2048 @ d32768 (c10) | 1145.89 ± 704.01 |   253.71 ± 303.06 |               |                  |   30938.70 ± 38848.63 |   30862.45 ± 38848.63 |   36802.27 ± 40701.68 |
| GPT-OSS-120B |   tg128 @ d32768 (c10) |    57.99 ± 34.12 |       6.59 ± 2.84 |  93.33 ± 9.43 |     10.53 ± 1.77 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d65535 (c1) |  1533.82 ± 13.70 |   1533.82 ± 13.70 |               |                  |     37795.50 ± 426.04 |     37719.25 ± 426.04 |     37935.42 ± 425.55 |
| GPT-OSS-120B |   ctx_tg @ d65535 (c1) |     22.50 ± 0.25 |      22.50 ± 0.25 |  24.00 ± 0.00 |     24.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d65535 (c1) |   927.84 ± 54.94 |    927.84 ± 54.94 |               |                  |      2290.98 ± 126.09 |      2214.73 ± 126.09 |      2419.17 ± 129.74 |
| GPT-OSS-120B |    tg128 @ d65535 (c1) |     23.37 ± 0.08 |      23.37 ± 0.08 |  24.00 ± 0.00 |     24.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d65535 (c2) |   1512.86 ± 4.52 |  1106.50 ± 347.38 |               |                  |   58159.69 ± 18240.56 |   58083.44 ± 18240.56 |   64523.39 ± 12066.59 |
| GPT-OSS-120B |   ctx_tg @ d65535 (c2) |      7.84 ± 0.08 |      10.23 ± 6.27 |  34.00 ± 0.00 |     18.00 ± 1.00 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d65535 (c2) |   875.34 ± 12.74 |   689.16 ± 227.94 |               |                  |     3412.71 ± 1103.36 |     3336.46 ± 1103.36 |       4653.66 ± 73.24 |
| GPT-OSS-120B |    tg128 @ d65535 (c2) |     35.18 ± 0.28 |      17.68 ± 0.24 |  37.33 ± 0.94 |     18.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B |   ctx_pp @ d65535 (c5) |   1509.22 ± 8.83 |   673.73 ± 417.41 |               |                  |  116009.08 ± 53604.98 |  115932.82 ± 53604.98 |  126089.75 ± 50107.77 |
| GPT-OSS-120B |   ctx_tg @ d65535 (c5) |      4.03 ± 0.01 |       3.27 ± 3.26 |  50.00 ± 0.00 |     12.80 ± 2.76 |                       |                       |                       |
| GPT-OSS-120B |   pp2048 @ d65535 (c5) |   928.15 ± 15.73 |   352.99 ± 287.19 |               |                  |     8691.52 ± 3547.57 |     8615.27 ± 3547.57 |     10994.34 ± 205.95 |
| GPT-OSS-120B |    tg128 @ d65535 (c5) |     51.12 ± 0.36 |      10.37 ± 0.14 |  60.00 ± 0.00 |     12.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |  ctx_pp @ d65535 (c10) |   1501.82 ± 2.95 |   431.77 ± 376.76 |               |                  | 212424.23 ± 110106.34 | 212347.98 ± 110106.34 | 223317.66 ± 108053.45 |
| GPT-OSS-120B |  ctx_tg @ d65535 (c10) |      3.51 ± 0.01 |       1.48 ± 1.79 |  60.00 ± 0.00 |      9.67 ± 3.57 |                       |                       |                       |
| GPT-OSS-120B |  pp2048 @ d65535 (c10) |     53.87 ± 2.55 |    45.96 ± 161.38 |               |                  | 199897.64 ± 115957.85 | 199821.39 ± 115957.85 | 210213.11 ± 114454.39 |
| GPT-OSS-120B |   tg128 @ d65535 (c10) |      3.46 ± 0.19 |       1.45 ± 1.80 |  60.00 ± 0.00 |      9.63 ± 3.69 |                       |                       |                       |
| GPT-OSS-120B |  ctx_pp @ d100000 (c1) |   1101.34 ± 3.59 |    1101.34 ± 3.59 |               |                  |     80240.81 ± 338.66 |     80164.56 ± 338.66 |     80387.52 ± 340.30 |
| GPT-OSS-120B |  ctx_tg @ d100000 (c1) |     19.88 ± 0.10 |      19.88 ± 0.10 |  21.00 ± 0.00 |     21.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |  pp2048 @ d100000 (c1) |    672.97 ± 7.14 |     672.97 ± 7.14 |               |                  |       3119.84 ± 32.52 |       3043.59 ± 32.52 |       3268.76 ± 29.85 |
| GPT-OSS-120B |   tg128 @ d100000 (c1) |     21.02 ± 0.22 |      21.02 ± 0.22 |  22.00 ± 0.00 |     22.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |  ctx_pp @ d100000 (c2) |   1094.12 ± 4.95 |   821.96 ± 274.22 |               |                  |  120909.00 ± 40246.23 |  120832.75 ± 40246.23 |  126257.61 ± 35127.68 |
| GPT-OSS-120B |  ctx_tg @ d100000 (c2) |      3.09 ± 0.04 |       7.45 ± 5.91 |  28.00 ± 0.00 |     15.50 ± 1.50 |                       |                       |                       |
| GPT-OSS-120B |  pp2048 @ d100000 (c2) |   609.31 ± 24.61 |   478.03 ± 167.39 |               |                  |     4930.24 ± 1643.24 |     4853.99 ± 1643.24 |      6702.46 ± 274.80 |
| GPT-OSS-120B |   tg128 @ d100000 (c2) |     29.59 ± 0.62 |      14.89 ± 0.38 |  32.00 ± 0.00 |     16.00 ± 0.00 |                       |                       |                       |
| GPT-OSS-120B |  ctx_pp @ d100000 (c5) |  1096.31 ± 10.59 |   499.70 ± 316.86 |               |                  | 242068.17 ± 114026.69 | 241991.92 ± 114026.69 | 251381.26 ± 111503.90 |
| GPT-OSS-120B |  ctx_tg @ d100000 (c5) |      1.87 ± 0.03 |       2.21 ± 2.87 |  39.67 ± 0.47 |     11.53 ± 3.22 |                       |                       |                       |
| GPT-OSS-120B |  pp2048 @ d100000 (c5) |    648.04 ± 3.95 |   265.59 ± 185.93 |               |                  |    10877.47 ± 4798.09 |    10801.22 ± 4798.09 |     15723.88 ± 128.48 |
| GPT-OSS-120B |   tg128 @ d100000 (c5) |     42.07 ± 0.75 |       8.52 ± 0.16 |  48.33 ± 2.36 |      9.67 ± 0.47 |                       |                       |                       |
| GPT-OSS-120B | ctx_pp @ d100000 (c10) |   1095.53 ± 4.07 |   322.46 ± 291.42 |               |                  | 442570.16 ± 231162.15 | 442493.91 ± 231162.15 | 452724.03 ± 229550.26 |
| GPT-OSS-120B | ctx_tg @ d100000 (c10) |      1.68 ± 0.01 |       0.99 ± 1.66 |  50.00 ± 0.00 |      8.70 ± 3.93 |                       |                       |                       |
| GPT-OSS-120B | pp2048 @ d100000 (c10) |     24.37 ± 0.11 |       7.13 ± 6.30 |               |                  | 458847.36 ± 239752.42 | 458771.11 ± 239752.42 | 468982.14 ± 237665.45 |
| GPT-OSS-120B |  tg128 @ d100000 (c10) |      1.64 ± 0.01 |       1.30 ± 2.58 |  45.67 ± 0.47 |      9.20 ± 4.95 |                       |                       |                       |
  Context Depth Analysis:

  --- depth=0 — Baseline (no context) ---
    Reading speed:  5,024 +/- 35 tok/s
    Writing speed:  137.7 +/- 1.3 tok/s  (peak: 163)
      -> Very fast — feels instant, smooth streaming
    Time to first token:  3606ms
      -> Noticeable wait (3.6s)

  --- depth=4,096 — depth 4096 ---
    Reading speed:  4,122 +/- 89 tok/s
    Writing speed:  121.5 +/- 2.1 tok/s  (peak: 147)
      -> -11.7% vs baseline (moderate slowdown)
      -> Very fast — feels instant, smooth streaming
    Time to first token:  4913ms
      -> Noticeable wait (4.9s)

  --- depth=8,192 — depth 8192 ---
    Reading speed:  3,345 +/- 57 tok/s
    Writing speed:  111.5 +/- 3.5 tok/s  (peak: 131)
    ⚠  -19.0% vs baseline — BANDWIDTH BOTTLENECK DETECTED
      -> Very fast — feels instant, smooth streaming
    Time to first token:  6030ms
      -> Noticeable wait (6.0s)

  --- depth=16,384 — Medium Log (~50 pages) ---
    Reading speed:  2,478 +/- 137 tok/s
    Writing speed:  101.3 +/- 5.6 tok/s  (peak: 126)
    ⚠  -26.4% vs baseline — BANDWIDTH BOTTLENECK DETECTED
      -> Very fast — feels instant, smooth streaming
    Time to first token:  8217ms
      -> Noticeable wait (8.2s)

  --- depth=32,768 — Massive Log (~100 pages) ---
    Reading speed:  1,768 +/- 23 tok/s
    Writing speed:  91.7 +/- 0.6 tok/s  (peak: 110)
    ⚠  -33.4% vs baseline — BANDWIDTH BOTTLENECK DETECTED
      -> Very fast — feels instant, smooth streaming
    Time to first token:  11475ms
      -> Noticeable wait (11.5s)

  --- depth=65,535 — Extreme Limit (~200 pages) ---
    Reading speed:  87 +/- 26 tok/s
    Writing speed:  5.3 +/- 1.4 tok/s  (peak: 68)
    ⚠  -96.1% vs baseline — BANDWIDTH BOTTLENECK DETECTED
      -> Slow — may feel sluggish for chat
    Time to first token:  123778ms
      -> Noticeable wait (123.8s)

  --- depth=100,000 — depth 100000 ---
    Reading speed:  24 tok/s
    Writing speed:  1.6 tok/s  (peak: 47)
    ⚠  -98.8% vs baseline — BANDWIDTH BOTTLENECK DETECTED
      -> Slow — may feel sluggish for chat
    Time to first token:  468727ms
      -> Noticeable wait (468.7s)

  Overall impact: depth 0 -> 100,000 = -98.8% generation speed
  Conclusion: Significant unified memory bandwidth bottleneck at depth 100,000

  e[2mJSON data : /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/GPT-OSS-120B_20260420_183825.jsone[0m
  e[2mForum table: /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/benchmarks/GPT-OSS-120B_20260420_183825.mde[0m
  Recipe YAML: /home/sparky/Docker/dgx-spark_lite-llm_llama-swap_vllm_llama-cpp_ollama/test-results/arena-submission/20260420_183825/GPT-OSS-120B/recipe.yaml

So now I can see that my setup sucks compared to the results of other users

View full benchmark at openai/gpt-oss-120b - Spark Arena Benchmark