11#! /usr/bin/env bash
2- # bench_paged_paths.sh — bench AITER on/off × prefill+decode × 8K/64K.
2+ # bench_paged_paths.sh — bench three attention paths × prefill+decode × 8K/64K.
33#
44# Mechanizes the "test both paged attention paths every commit" rule. Unlike
55# profile_prefill.sh this does NOT use rocprof (which slows the GPU ~2x and
66# hides production-curve regressions); it runs llama-server clean and reads
77# timings off the /v1/completions response.
88#
99# Two axes:
10- # aiter ∈ {0, 1} — MAD_USE_AITER=1 forces the AITER unified_attention
11- # path. AITER currently asserts F16 KV (MAD-199), so
12- # the AITER row uses --cache-type-k f16; the non-AITER
13- # row uses the production TURBO4 cache type.
14- # ctx ∈ {8K, 64K} — short prompt + long prompt, covers the bend in the
15- # prefill curve where the tile kernel kicks in.
10+ # path ∈ {paged_turbo4, paged_aiter_f16, vanilla_f16}
11+ # — paged_turbo4: production-tier path
12+ # (--kv-tiered 100,0,0 --kv-tier-paged-blocks,
13+ # TURBO4 quantized KV, MAD_USE_AITER unset)
14+ # — paged_aiter_f16: AITER unified_attention
15+ # (--kv-tiered 100,0,0 --kv-tier-paged-blocks,
16+ # F16 KV per MAD-199, MAD_USE_AITER=1)
17+ # — vanilla_f16: stock llama.cpp F16 KV
18+ # (no --kv-tiered, no --kv-tier-paged-blocks)
19+ # tracks upstream / mainline-comparable perf.
20+ # ctx ∈ {8K, 64K} — short prompt + long prompt, covers the bend in
21+ # the prefill curve where the tile kernel kicks in.
1622#
1723# Output: tests/perf-baseline/paged-paths/<short_commit>-<UTC_timestamp>.json
1824#
@@ -69,9 +75,19 @@ prompt_file_for() {
6975import json, sys
7076target_tokens, out_path, n_predict = sys.argv[1:4]
7177target_tokens = int(target_tokens); n_predict = int(n_predict)
72- # ~3.6 chars/token avg on Qwen for English prose. Pad a bit so we land at
73- # or just above the target.
74- char_target = int(target_tokens * 3.8)
78+ # Measured: Qwen BPE on our synthetic English corpus = ~4.78 chars/token.
79+ # Two competing constraints:
80+ # - For small targets (8K), the decode-gate at mt_pagedattn.cu:1028 requires
81+ # max_ctx_len >= 8192. Below that, decode falls to scalar fallback
82+ # (~32 t/s instead of ~50 t/s on Qwen3.6-35B-A3B). Need to overshoot.
83+ # - For large targets (64K), admission (MAD-141) needs prompt < ~0.5x ctx.
84+ # Overshooting drives the request past the hot pool and stalls.
85+ # So: aggressive overshoot at small target (clears the gate), undershoot at
86+ # large target (stays admissible). Both land far above the gate threshold.
87+ if target_tokens <= 16384:
88+ char_target = int(target_tokens * 5.5)
89+ else:
90+ char_target = int(target_tokens * 4.5)
7591seed = (
7692 "The pagedattn benchmark needles its way through tiered KV cache, "
7793 "stitching tile kernels to flash-decode and watching where the bytes "
@@ -84,6 +100,11 @@ body = {
84100 "temperature": 0.0,
85101 "cache_prompt": False,
86102 "stream": False,
103+ # The synthetic prompt sometimes hits an early "stop" finish_reason after
104+ # ~2 generated tokens (e.g., \n\n). Force the full decode window so the
105+ # bench actually measures n_predict tokens of decode throughput.
106+ "ignore_eos": True,
107+ "stop": [],
87108}
88109with open(out_path, "w") as f:
89110 json.dump(body, f)
92113}
93114
94115# ----------------------------------------------------------------------------
95- # Run one (aiter , ctx) cell. Echoes a single JSON object to stdout.
116+ # Run one (path , ctx) cell. Echoes a single JSON object to stdout.
96117# ----------------------------------------------------------------------------
97118run_cell () {
98- local aiter =" $1 " target_tokens=" $2 "
119+ local path =" $1 " target_tokens=" $2 "
99120 local prompt_path; prompt_path=" $( prompt_file_for " ${target_tokens} " ) "
100121
101- local label=" aiter ${aiter } _ctx${target_tokens} "
122+ local label=" ${path } _ctx${target_tokens} "
102123 local server_log=" ${WORK_DIR} /${label} .server.log"
103124 local resp_path=" ${WORK_DIR} /${label} .resp.json"
104125 rm -f " ${server_log} " " ${resp_path} "
105126
106- # AITER currently requires F16 KV (MAD-199). Non-AITER uses prod TURBO4.
107- local cache_type
108- if [[ " ${aiter} " == " 1" ]]; then
109- cache_type=" f16"
110- else
111- cache_type=" turbo4"
112- fi
113- # ctx-size: must be >= target tokens + n_predict. Round up for headroom.
114- local ctx_size=$(( target_tokens + N_PREDICT + 256 ))
115- if (( ctx_size < 8192 )) ; then ctx_size=8192; fi
127+ # Per-path config: cache type, AITER toggle, KV-mode flags.
128+ local cache_type aiter_env tier_flags
129+ case " ${path} " in
130+ paged_turbo4)
131+ cache_type=" turbo4" ; aiter_env=" 0"
132+ tier_flags=" --kv-tiered 100,0,0 --kv-tier-paged-blocks --ctx-checkpoints 0"
133+ ;;
134+ paged_aiter_f16)
135+ cache_type=" f16" ; aiter_env=" 1"
136+ tier_flags=" --kv-tiered 100,0,0 --kv-tier-paged-blocks --ctx-checkpoints 0"
137+ ;;
138+ vanilla_f16)
139+ # Stock llama.cpp: no tiered KV, no paged blocks. F16 cache.
140+ # This is the upstream-comparable baseline — what mainline
141+ # llama.cpp would do on the same hardware/model.
142+ cache_type=" f16" ; aiter_env=" 0"
143+ tier_flags=" "
144+ ;;
145+ * ) echo " ERROR: unknown path '${path} '" >&2 ; return 1 ;;
146+ esac
147+
148+ # ctx-size: with paged + --kv-tiered 100,0,0 the entire KV pool is hot —
149+ # no tier to demote to — so MAD-141 admission needs ~2x prompt-size
150+ # headroom (a 1.5x ratio failed mid-prefill on the 64K cell). Same 2x
151+ # ratio is fine for vanilla_f16 (no admission machinery there, but
152+ # consistent ctx makes paths comparable). At 2x, F16 at 64K target ≈
153+ # 8GB KV which fits alongside the 35B Q4 weights on R9700. 32K floor
154+ # keeps the 8K cell on the same headroom curve.
155+ local ctx_size=$(( target_tokens * 2 ))
156+ if (( ctx_size < 32768 )) ; then ctx_size=32768; fi
116157
117- echo " [${label} ] launching server (cache=${cache_type} , ctx=${ctx_size} )..." >&2
158+ echo " [${label} ] launching server (cache=${cache_type} , ctx=${ctx_size} , aiter= ${aiter_env} )..." >&2
118159
119- MAD_USE_AITER=" ${aiter} " setsid nohup " ${LLAMA_SERVER} " \
160+ # shellcheck disable=SC2086 # tier_flags is intentionally word-split
161+ MAD_USE_AITER=" ${aiter_env} " setsid nohup " ${LLAMA_SERVER} " \
120162 --model " ${MODEL_PATH} " \
121163 --device ROCm0 --n-gpu-layers 999 \
122164 --ctx-size " ${ctx_size} " --parallel 1 \
123- --kv-tiered 100,0,0 \
165+ ${tier_flags} \
124166 --cache-type-k " ${cache_type} " --cache-type-v " ${cache_type} " \
125167 --flash-attn on --no-mmap --no-warmup \
126- --cache-ram 0 --ctx-checkpoints 0 \
168+ --cache-ram 0 \
127169 --host 127.0.0.1 --port " ${PORT} " \
128170 --timeout 3600 --alias bench \
129171 > " ${server_log} " 2>&1 < /dev/null &
@@ -180,12 +222,13 @@ run_cell() {
180222 return 1
181223 fi
182224
183- python3 - " ${aiter } " " ${target_tokens} " " ${cache_type} " " ${ctx_size} " " ${resp_path} " << 'PY '
225+ python3 - " ${path} " " ${aiter_env }" " ${target_tokens} " " ${cache_type} " " ${ctx_size} " " ${resp_path} " << 'PY '
184226import json, sys
185- aiter, target_tokens, cache_type, ctx_size, resp_path = sys.argv[1:6 ]
227+ path, aiter, target_tokens, cache_type, ctx_size, resp_path = sys.argv[1:7 ]
186228r = json.load(open(resp_path))
187229t = r.get("timings", {})
188230out = {
231+ "path": path,
189232 "aiter": int(aiter),
190233 "target_tokens": int(target_tokens),
191234 "cache_type": cache_type,
@@ -209,9 +252,9 @@ echo " out=${OUT_PATH}" >&2
209252
210253cells=()
211254IFS=' ,' read -r -a CTX_ARR <<< " ${CTX_SIZES}"
212- for aiter in 0 1 ; do
255+ for path in paged_turbo4 paged_aiter_f16 vanilla_f16 ; do
213256 for ctx in " ${CTX_ARR[@]} " ; do
214- cell_json=" $( run_cell " ${aiter } " " ${ctx} " ) "
257+ cell_json=" $( run_cell " ${path } " " ${ctx} " ) "
215258 cells+=(" ${cell_json} " )
216259 done
217260done
0 commit comments