-
Notifications
You must be signed in to change notification settings - Fork 273
Expand file tree
/
Copy pathbenchmark_lib.sh
More file actions
2334 lines (2129 loc) · 88.2 KB
/
Copy pathbenchmark_lib.sh
File metadata and controls
2334 lines (2129 loc) · 88.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# Shared benchmarking utilities for InferenceX
# Keep Python bytecode out of the mounted workspace. Benchmark jobs often run as
# root inside containers, and root-owned cache directories break future checkout
# cleanup on self-hosted runners.
export PYTHONDONTWRITEBYTECODE=1
export PYTHONPYCACHEPREFIX="${PYTHONPYCACHEPREFIX:-/tmp/inferencex-pycache}"
mkdir -p "$PYTHONPYCACHEPREFIX" 2>/dev/null || true
# Inference server port shared by every benchmark recipe. Launchers that need
# a non-default value (e.g. launch_mi355x-amds.sh derives PORT from RUNNER_NAME
# to avoid collisions across concurrent gh-runners on a shared host) set PORT
# themselves before sourcing this file; the `:-` fallback only kicks in when
# nothing upstream set it.
export PORT="${PORT:-8888}"
agentic_kv_offload_enabled() {
if [[ -z "${KV_OFFLOADING+x}" || -z "$KV_OFFLOADING" ]]; then
echo "Error: KV_OFFLOADING must be set for agentic benchmarks" >&2
exit 1
fi
[[ "$KV_OFFLOADING" != "none" ]]
}
require_agentic_kv_offload_none() {
if agentic_kv_offload_enabled; then
echo "Error: expected KV_OFFLOADING=none, got '$KV_OFFLOADING'" >&2
exit 1
fi
if [[ -n "${KV_OFFLOAD_BACKEND:-}" ]]; then
echo "Error: KV_OFFLOAD_BACKEND must be empty when KV_OFFLOADING=none" >&2
exit 1
fi
}
require_agentic_kv_offload_backend() {
local expected_backend="$1"
if [[ -z "${KV_OFFLOADING+x}" || -z "$KV_OFFLOADING" ]]; then
echo "Error: KV_OFFLOADING must be set for agentic benchmarks" >&2
exit 1
fi
case "$KV_OFFLOADING" in
none)
if [[ -n "${KV_OFFLOAD_BACKEND:-}" ]]; then
echo "Error: KV_OFFLOAD_BACKEND must be empty when KV_OFFLOADING=none" >&2
exit 1
fi
return 1
;;
dram)
if [[ "${KV_OFFLOAD_BACKEND:-}" != "$expected_backend" ]]; then
echo "Error: expected KV_OFFLOAD_BACKEND=$expected_backend when KV_OFFLOADING=dram, got '${KV_OFFLOAD_BACKEND:-}'" >&2
exit 1
fi
if [[ ! "${TOTAL_CPU_DRAM_GB:-}" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: DRAM KV offloading requires a positive TOTAL_CPU_DRAM_GB capacity" >&2
exit 1
fi
return 0
;;
*)
echo "Error: unsupported KV_OFFLOADING value '$KV_OFFLOADING' (expected one of: none, dram)" >&2
exit 1
;;
esac
}
# Agentic replays must use the model's native context limit. Ignore inherited
# workflow or shell overrides so neither the server nor AIPerf applies a cap.
_benchmark_caller="${BASH_SOURCE[1]:-}"
if [[ "$_benchmark_caller" == */agentic/* ||
"$_benchmark_caller" == */agentic_*.sh ||
"${IS_AGENTIC:-0}" == "1" ||
"${SCENARIO_TYPE:-}" == "agentic-coding" ]]; then
unset MAX_MODEL_LEN
if [[ -z "${KV_OFFLOADING+x}" || -z "$KV_OFFLOADING" ]]; then
echo "Error: KV_OFFLOADING must be set for agentic benchmarks" >&2
exit 1
fi
case "$KV_OFFLOADING" in
none)
if [[ -n "${KV_OFFLOAD_BACKEND:-}" ]]; then
echo "Error: KV_OFFLOAD_BACKEND must be empty when KV_OFFLOADING=none" >&2
exit 1
fi
;;
dram)
if [[ -z "${KV_OFFLOAD_BACKEND:-}" || "${KV_OFFLOAD_BACKEND:-}" == "none" ]]; then
echo "Error: KV_OFFLOAD_BACKEND is required when KV_OFFLOADING=dram" >&2
exit 1
fi
if [[ ! "${TOTAL_CPU_DRAM_GB:-}" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: DRAM KV offloading requires a positive configured TOTAL_CPU_DRAM_GB capacity" >&2
exit 1
fi
;;
*)
echo "Error: unsupported KV_OFFLOADING value '$KV_OFFLOADING' (expected one of: none, dram)" >&2
exit 1
;;
esac
fi
unset _benchmark_caller
# --------------------------------
# GPU monitoring helpers
# --------------------------------
GPU_MONITOR_PID=""
GPU_MONITOR_VENDOR=""
GPU_MONITOR_INTERVAL=1
GPU_METRICS_CSV="${GPU_METRICS_CSV:-gpu_metrics.csv}"
NVIDIA_GPU_MONITOR_QUERY="timestamp,index,power.draw,temperature.gpu,clocks.current.sm,clocks.current.memory,utilization.gpu,utilization.memory"
export GPU_METRICS_CSV
# Start background GPU monitoring that logs metrics every second to CSV.
# Auto-detects NVIDIA (nvidia-smi) or AMD (amd-smi) GPUs.
# Usage: start_gpu_monitor [--output /path/to/output.csv] [--interval 1]
start_gpu_monitor() {
local output="$GPU_METRICS_CSV"
local interval=1
while [[ $# -gt 0 ]]; do
case $1 in
--output) output="$2"; shift 2 ;;
--interval) interval="$2"; shift 2 ;;
*) shift ;;
esac
done
GPU_METRICS_CSV="$output"
GPU_MONITOR_INTERVAL="$interval"
export GPU_METRICS_CSV
if command -v nvidia-smi &>/dev/null; then
GPU_MONITOR_VENDOR="nvidia"
nvidia-smi --query-gpu="$NVIDIA_GPU_MONITOR_QUERY" \
--format=csv -l "$interval" > "$output" 2>/dev/null &
GPU_MONITOR_PID=$!
echo "[GPU Monitor] Started NVIDIA (PID=$GPU_MONITOR_PID, interval=${interval}s, output=$output)"
elif command -v amd-smi &>/dev/null; then
GPU_MONITOR_VENDOR="amd"
# Use amd-smi native watch mode (-w) which includes timestamps automatically.
# PYTHONUNBUFFERED defeats the tool's own stdout block buffering (amd-smi is
# Python; measured on MI355X: trailing ticks were lost at kill without it).
# Pipe through awk to: skip preamble lines, keep first CSV header, skip repeated
# headers, and flush every row so killing the pipe cannot discard buffered samples.
PYTHONUNBUFFERED=1 amd-smi metric -p -c -t -u -w "$interval" --csv 2>/dev/null \
| awk '/^timestamp,/{if(!h){print;h=1};next} h{print;fflush()}' > "$output" &
GPU_MONITOR_PID=$!
# Hardware energy-accumulator + identity snapshots; the end-side twin in
# stop_gpu_monitor lets auditors cross-check the integrated energy
# against the accumulator delta.
_write_amd_smi_sidecar "${output%.csv}_energy_start.csv" metric -E --csv
_write_amd_smi_sidecar "${output%.csv}_identity.json" static --json
echo "[GPU Monitor] Started AMD (PID=$GPU_MONITOR_PID, interval=${interval}s, output=$output)"
else
GPU_MONITOR_VENDOR=""
echo "[GPU Monitor] No GPU monitoring tool found (nvidia-smi or amd-smi), skipping"
return 0
fi
}
# Stop the background GPU monitor and report file size.
stop_gpu_monitor() {
if [[ -n "$GPU_MONITOR_PID" ]] && kill -0 "$GPU_MONITOR_PID" 2>/dev/null; then
# benchmark_end_time_unix is recorded shortly before the benchmark
# process exits, so the stream must cover one more sample past it for
# deterministic boundary interpolation. NVIDIA appends a one-shot
# post-exit sample below; amd-smi one-shot CSV has no timestamp column,
# so the AMD path instead lets the watch stream emit final ticks before
# the kill. Two extra intervals: amd-smi stamps integer seconds, so a
# tick in the same second as the window end still fails bracketing —
# the stream needs a tick at the NEXT whole second (measured on MI355X:
# end=...153.325 vs last sample ...153.0).
if [[ "$GPU_MONITOR_VENDOR" == "amd" ]]; then
sleep $(( ${GPU_MONITOR_INTERVAL:-1} + 2 ))
fi
kill "$GPU_MONITOR_PID" 2>/dev/null
wait "$GPU_MONITOR_PID" 2>/dev/null || true
case "$GPU_MONITOR_VENDOR" in
nvidia)
if _repair_truncated_gpu_metrics_tail; then
nvidia-smi --query-gpu="$NVIDIA_GPU_MONITOR_QUERY" \
--format=csv,noheader >> "$GPU_METRICS_CSV" 2>/dev/null ||
echo "[GPU Monitor] Warning: final NVIDIA sample failed" >&2
fi
;;
amd)
_repair_truncated_gpu_metrics_tail || true
_write_amd_smi_sidecar "${GPU_METRICS_CSV%.csv}_energy_end.csv" metric -E --csv
;;
esac
echo "[GPU Monitor] Stopped (PID=$GPU_MONITOR_PID)"
if [[ -f "$GPU_METRICS_CSV" ]]; then
local lines
lines=$(wc -l < "$GPU_METRICS_CSV")
echo "[GPU Monitor] Collected $lines rows -> $GPU_METRICS_CSV"
fi
fi
GPU_MONITOR_PID=""
GPU_MONITOR_VENDOR=""
}
# Drop a partial trailing row left behind when the monitor dies mid-write.
# Returns non-zero when a truncated row was detected but could not be removed.
_repair_truncated_gpu_metrics_tail() {
local repaired_metrics="${GPU_METRICS_CSV}.repair.$$"
if [[ -s "$GPU_METRICS_CSV" ]] &&
! tail -c 1 "$GPU_METRICS_CSV" | grep -q '^$'; then
if sed '$d' "$GPU_METRICS_CSV" > "$repaired_metrics" &&
mv "$repaired_metrics" "$GPU_METRICS_CSV"; then
echo "[GPU Monitor] Dropped truncated trailing sample"
else
rm -f "$repaired_metrics"
echo "[GPU Monitor] Warning: could not repair truncated trailing sample" >&2
return 1
fi
fi
return 0
}
# Write one best-effort amd-smi snapshot; remove the file rather than keep a
# partial one when the invocation fails.
_write_amd_smi_sidecar() {
local out="$1"
shift
if ! amd-smi "$@" > "$out" 2>/dev/null; then
rm -f "$out"
echo "[GPU Monitor] Warning: amd-smi $1 sidecar failed" >&2
fi
}
# Block until the GPUs have released a prior job's memory before starting a run.
# Polls rocm-smi VRAM% every 10s for up to 15 minutes; succeeds once the busiest
# GPU is at <=10% VRAM, otherwise returns 1 so the caller aborts rather than
# starting a benchmark on GPUs still draining the previous run's memory.
wait_for_amd_gpu_clean() {
local gpu_clean=false vram_max i
for i in $(seq 1 90); do
vram_max=$(rocm-smi --showmemuse 2>/dev/null \
| grep -oE "GPU Memory Allocated \(VRAM%\): [0-9]+" \
| awk '{if ($NF > m) m = $NF} END {print m+0}')
if [ "${vram_max:-0}" -le 10 ]; then
echo "GPUs clean (vram%max=$vram_max after $((i * 10))s)"
gpu_clean=true
break
fi
echo "waiting for prior-job GPU memory reclaim: vram%max=$vram_max"
sleep 10
done
if [ "$gpu_clean" != "true" ]; then
echo "Error: GPUs still draining prior job's memory after 15min" >&2
return 1
fi
}
# Return success only while a PID exists and is not a zombie waiting to be
# reaped. `kill -0` alone treats zombies as live processes.
_background_process_is_running() {
local pid="$1"
local state
kill -0 "$pid" 2>/dev/null || return 1
state=$(ps -o stat= -p "$pid" 2>/dev/null) || return 1
[[ -n "$state" && "${state:0:1}" != "Z" ]]
}
_background_process_descendants() {
local parent_pid="$1"
local child_pid
while read -r child_pid; do
[[ -n "$child_pid" ]] || continue
echo "$child_pid"
_background_process_descendants "$child_pid"
done < <(pgrep -P "$parent_pid" 2>/dev/null || true)
}
# Stop a background service and every process that descended from it. Capture
# descendants before terminating the root because orphaned workers are
# reparented and can otherwise keep a Slurm step alive after the benchmark
# script exits.
stop_background_process_tree() {
local root_pid="${1:-}"
local label="${2:-background process}"
local grace_seconds="${3:-30}"
if [[ ! "$root_pid" =~ ^[1-9][0-9]*$ ]] || ! _background_process_is_running "$root_pid"; then
return 0
fi
local descendants
local child_pid
descendants=$(_background_process_descendants "$root_pid")
echo "Stopping $label (PID=$root_pid)..."
kill -TERM "$root_pid" 2>/dev/null || true
local deadline=$((SECONDS + grace_seconds))
while _background_process_is_running "$root_pid" && [[ $SECONDS -lt $deadline ]]; do
sleep 1
done
local forced_stop=false
while read -r child_pid; do
[[ -n "$child_pid" ]] || continue
if _background_process_is_running "$child_pid"; then
if [[ "$forced_stop" == "false" ]]; then
echo "Force-stopping remaining $label processes."
forced_stop=true
fi
echo " PID=$child_pid"
kill -KILL "$child_pid" 2>/dev/null || true
fi
done <<EOF
$root_pid
$descendants
EOF
wait "$root_pid" 2>/dev/null || true
echo "Stopped $label."
}
# Check if required environment variables are set
# Usage: check_env_vars VAR1 VAR2 VAR3 ...
# Exits with code 1 if any variable is not set
check_env_vars() {
local missing_vars=()
for var_name in "$@"; do
if [[ -z "${!var_name:-}" ]]; then
missing_vars+=("$var_name")
fi
done
if [[ ${#missing_vars[@]} -gt 0 ]]; then
echo "Error: The following required environment variables are not set:"
for var in "${missing_vars[@]}"; do
echo " - $var"
done
exit 1
fi
}
# Poll an HTTP endpoint while streaming the owning process log.
# Required: --endpoint, --log, --pid. A zero timeout waits indefinitely.
wait_for_ready() {
set +x
local endpoint=""
local process_log=""
local process_pid=""
local sleep_interval=5
local timeout=0
while [[ $# -gt 0 ]]; do
case $1 in
--endpoint)
endpoint="$2"
shift 2
;;
--log)
process_log="$2"
shift 2
;;
--pid)
process_pid="$2"
shift 2
;;
--sleep-interval)
sleep_interval="$2"
shift 2
;;
--timeout)
timeout="$2"
shift 2
;;
*)
echo "Unknown parameter: $1"
return 1
;;
esac
done
if [[ -z "$endpoint" ]]; then
echo "Error: --endpoint is required"
return 1
fi
if [[ -z "$process_log" ]]; then
echo "Error: --log is required"
return 1
fi
if [[ -z "$process_pid" ]]; then
echo "Error: --pid is required"
return 1
fi
if [[ ! "$sleep_interval" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: --sleep-interval must be a positive integer"
return 1
fi
if [[ ! "$timeout" =~ ^[0-9]+$ ]]; then
echo "Error: --timeout must be a non-negative integer"
return 1
fi
local deadline=0
if [[ "$timeout" -gt 0 ]]; then
deadline=$((SECONDS + timeout))
fi
while [[ ! -f "$process_log" ]]; do
if ! kill -0 "$process_pid" 2>/dev/null; then
echo "Process died before creating $process_log." >&2
exit 1
fi
if [[ "$deadline" -gt 0 && "$SECONDS" -ge "$deadline" ]]; then
echo "Timed out waiting for $endpoint." >&2
exit 1
fi
sleep 1
done
tail -f -n +1 "$process_log" &
local tail_pid=$!
until curl --output /dev/null --silent --fail "$endpoint"; do
if ! kill -0 "$process_pid" 2>/dev/null; then
echo "Process died before $endpoint became ready." >&2
kill "$tail_pid" 2>/dev/null || true
exit 1
fi
if [[ "$deadline" -gt 0 && "$SECONDS" -ge "$deadline" ]]; then
echo "Timed out waiting for $endpoint." >&2
kill "$tail_pid" 2>/dev/null || true
exit 1
fi
sleep "$sleep_interval"
done
kill "$tail_pid" 2>/dev/null || true
wait "$tail_pid" 2>/dev/null || true
}
wait_for_server_ready() {
local port=""
local server_log=""
local server_pid=""
local sleep_interval=5
while [[ $# -gt 0 ]]; do
case $1 in
--port) port="$2"; shift 2 ;;
--server-log) server_log="$2"; shift 2 ;;
--server-pid) server_pid="$2"; shift 2 ;;
--sleep-interval) sleep_interval="$2"; shift 2 ;;
*) echo "Unknown parameter: $1"; return 1 ;;
esac
done
if [[ -z "$port" || -z "$server_log" || -z "$server_pid" ]]; then
echo "Error: --port, --server-log, and --server-pid are required"
return 1
fi
wait_for_ready \
--endpoint "http://0.0.0.0:${port}/health" \
--log "$server_log" \
--pid "$server_pid" \
--sleep-interval "$sleep_interval"
}
# Persist an argv array in shell-replayable form.
write_command() {
local output_file="$1"
shift
printf '%q ' "$@" | tee "$output_file"
printf '\n' | tee -a "$output_file"
}
append_command() {
local output_file="$1"
shift
printf '%q ' "$@" >> "$output_file"
printf '\n' >> "$output_file"
}
# Persist an argv array in shell-replayable form.
write_command() {
local output_file="$1"
shift
printf '%q ' "$@" | tee "$output_file"
printf '\n' | tee -a "$output_file"
}
# Run benchmark serving with standardized parameters
# All parameters are required except --endpoint, --use-chat-template, --dsv4, and --trust-remote-code
# Parameters:
# --model: Model name
# --port: Server port
# --backend: Backend type - e.g., 'vllm' or 'openai'
# --endpoint: Optional API endpoint override
# --input-len: Random input sequence length
# --output-len: Random output sequence length
# --random-range-ratio: Random range ratio
# --num-prompts: Number of prompts
# --max-concurrency: Max concurrency
# --result-filename: Result filename without extension
# --result-dir: Result directory
# --use-chat-template: Optional flag to enable chat template
# --dsv4: Optional flag to use the DeepSeek-V4 chat template
# (encoding_dsv4.py) instead of the tokenizer's built-in jinja
# template. Implies --use-chat-template.
# --trust-remote-code: Optional flag to trust remote code from HuggingFace
# --server-pid: Optional server process ID to monitor during benchmark
run_benchmark_serving() {
# In eval-only mode, skip the throughput benchmark entirely.
if [ "${EVAL_ONLY}" = "true" ]; then
echo "EVAL_ONLY mode: skipping throughput benchmark"
return 0
fi
set +x
local model=""
local port=""
local backend=""
local endpoint=""
local input_len=""
local output_len=""
local random_range_ratio=""
local num_prompts=""
local max_concurrency=""
local result_filename=""
local result_dir=""
local workspace_dir=""
local use_chat_template=false
local dsv4=false
local trust_remote_code=false
local server_pid=""
local tokenizer=""
local tokenizer_mode=""
while [[ $# -gt 0 ]]; do
case $1 in
--model)
model="$2"
shift 2
;;
--port)
port="$2"
shift 2
;;
--backend)
backend="$2"
shift 2
;;
--endpoint)
endpoint="$2"
shift 2
;;
--input-len)
input_len="$2"
shift 2
;;
--output-len)
output_len="$2"
shift 2
;;
--random-range-ratio)
random_range_ratio="$2"
shift 2
;;
--num-prompts)
num_prompts="$2"
shift 2
;;
--max-concurrency)
max_concurrency="$2"
shift 2
;;
--result-filename)
result_filename="$2"
shift 2
;;
--result-dir)
result_dir="$2"
shift 2
;;
--bench-serving-dir)
workspace_dir="$2"
shift 2
;;
--use-chat-template)
use_chat_template=true
shift
;;
--dsv4)
dsv4=true
use_chat_template=true
shift
;;
--trust-remote-code)
trust_remote_code=true
shift
;;
--server-pid)
server_pid="$2"
shift 2
;;
--tokenizer)
tokenizer="$2"
shift 2
;;
--tokenizer-mode)
tokenizer_mode="$2"
shift 2
;;
*)
echo "Unknown parameter: $1"
return 1
;;
esac
done
# Validate all required parameters
if [[ -z "$model" ]]; then
echo "Error: --model is required"
return 1
fi
if [[ -z "$port" ]]; then
echo "Error: --port is required"
return 1
fi
if [[ -z "$backend" ]]; then
echo "Error: --backend is required"
return 1
fi
if [[ -z "$input_len" ]]; then
echo "Error: --input-len is required"
return 1
fi
if [[ -z "$output_len" ]]; then
echo "Error: --output-len is required"
return 1
fi
if [[ -z "$random_range_ratio" ]]; then
echo "Error: --random-range-ratio is required"
return 1
fi
if [[ -z "$num_prompts" ]]; then
echo "Error: --num-prompts is required"
return 1
fi
if [[ -z "$max_concurrency" ]]; then
echo "Error: --max-concurrency is required"
return 1
fi
if [[ -z "$result_filename" ]]; then
echo "Error: --result-filename is required"
return 1
fi
if [[ -z "$result_dir" ]]; then
echo "Error: --result-dir is required"
return 1
fi
if [[ -z "$workspace_dir" ]]; then
workspace_dir=$(pwd)
fi
# Profiling support: when PROFILE=1, ensure profiler dir exists, add --profile flag,
# and cap num_prompts to keep traces small.
local profile_flag=()
if [[ "${PROFILE:-}" == "1" ]]; then
local _prof_dir="${SGLANG_TORCH_PROFILER_DIR:-${VLLM_TORCH_PROFILER_DIR:-}}"
if [[ -n "$_prof_dir" ]]; then
mkdir -p "$_prof_dir"
fi
profile_flag+=(--profile)
num_prompts="$max_concurrency"
fi
# Build benchmark command
local benchmark_cmd=(
python3 "$workspace_dir/utils/bench_serving/benchmark_serving.py"
--model "$model"
--backend "$backend"
--base-url "http://0.0.0.0:$port"
--dataset-name random
--random-input-len "$input_len"
--random-output-len "$output_len"
--random-range-ratio "$random_range_ratio"
--num-prompts "$num_prompts"
--max-concurrency "$max_concurrency"
--request-rate inf
--ignore-eos
"${profile_flag[@]}"
--save-result
--num-warmups "$((2 * max_concurrency))" \
--percentile-metrics 'ttft,tpot,itl,e2el'
--result-dir "$result_dir"
--result-filename "$result_filename.json"
)
if [[ -n "$endpoint" ]]; then
benchmark_cmd+=(--endpoint "$endpoint")
fi
# Add --use-chat-template if requested
if [[ "$use_chat_template" == true ]]; then
benchmark_cmd+=(--use-chat-template)
fi
# Add --dsv4 if requested (requires --use-chat-template, which we
# auto-enable when --dsv4 is passed in).
if [[ "$dsv4" == true ]]; then
benchmark_cmd+=(--dsv4)
fi
# Add --trust-remote-code if requested
if [[ "$trust_remote_code" == true ]]; then
benchmark_cmd+=(--trust-remote-code)
fi
if [[ -n "$tokenizer" ]]; then
benchmark_cmd+=(--tokenizer "$tokenizer")
fi
if [[ -n "$tokenizer_mode" ]]; then
benchmark_cmd+=(--tokenizer-mode "$tokenizer_mode")
fi
# Run benchmark with optional server monitoring
set -x
if [[ -n "$server_pid" ]]; then
# Run benchmark in background and monitor server health
"${benchmark_cmd[@]}" &
local benchmark_pid=$!
# Monitor loop: check both benchmark and server status
while kill -0 "$benchmark_pid" 2>/dev/null; do
if ! kill -0 "$server_pid" 2>/dev/null; then
echo "ERROR: Server process $server_pid died during benchmark"
kill "$benchmark_pid" 2>/dev/null
wait "$benchmark_pid" 2>/dev/null
set +x
return 1
fi
sleep 2
done
# Benchmark finished, get its exit code
wait "$benchmark_pid"
local benchmark_exit_code=$?
else
# No server monitoring, run benchmark directly
"${benchmark_cmd[@]}"
local benchmark_exit_code=$?
fi
set +x
# If profiling, move trace to relay-upload location
if [[ "${PROFILE:-}" == "1" ]]; then
move_profile_trace_for_relay
fi
return $benchmark_exit_code
}
# --------------------------------
# Profiling trace helpers
# --------------------------------
_find_latest_profile_trace() {
local latest=""
local dir="" candidate="" base=""
local -a search_roots=()
for dir in "$@"; do
search_roots=()
if [[ -d "$dir" ]]; then
search_roots+=("$dir")
fi
if [[ -d "$dir/profiles" ]]; then
search_roots+=("$dir/profiles")
fi
if [[ ${#search_roots[@]} -eq 0 ]]; then
continue
fi
while IFS= read -r -d '' candidate; do
base="$(basename "$candidate")"
if [[ "$base" == profile_*.trace.json.gz ]]; then
continue
fi
if [[ -z "$latest" || "$candidate" -nt "$latest" ]]; then
latest="$candidate"
fi
done < <(
find "${search_roots[@]}" -maxdepth 1 -type f \
\( -name "*.trace.json" -o -name "*.trace.json.gz" -o -name "*trace*.json" -o -name "*trace*.json.gz" -o -name "*profile*.json" -o -name "*profile*.json.gz" \) \
-print0 2>/dev/null
)
done
printf '%s' "$latest"
}
# Move profiler trace into a stable workspace path for workflow relay/upload.
move_profile_trace_for_relay() {
if [[ "${PROFILE:-}" != "1" ]]; then
return 0
fi
if [[ -z "${RESULT_FILENAME:-}" ]]; then
echo "[PROFILE] RESULT_FILENAME is not set; skipping relay trace staging." >&2
return 0
fi
local sglang_dir="${SGLANG_TORCH_PROFILER_DIR:-/workspace}"
local vllm_dir="${VLLM_TORCH_PROFILER_DIR:-/workspace}"
local -a search_dirs=()
local dir="" existing=""
local seen=0
for dir in "$sglang_dir" "$vllm_dir" "/workspace"; do
if [[ -z "$dir" ]]; then
continue
fi
seen=0
for existing in "${search_dirs[@]}"; do
if [[ "$existing" == "$dir" ]]; then
seen=1
break
fi
done
if [[ "$seen" -eq 0 ]]; then
search_dirs+=("$dir")
fi
done
local trace_file=""
local wait_attempts=10
for (( i=1; i<=wait_attempts; i++ )); do
trace_file="$(_find_latest_profile_trace "${search_dirs[@]}")"
if [[ -n "$trace_file" ]]; then
break
fi
sleep 10
done
if [[ -z "$trace_file" ]]; then
echo "[PROFILE] No trace found for relay under: ${search_dirs[*]}" >&2
return 0
fi
local dest_trace="/workspace/profile_${RESULT_FILENAME}.trace.json.gz"
if [[ "$trace_file" == *.gz ]]; then
cp -f "$trace_file" "$dest_trace"
else
gzip -c "$trace_file" > "$dest_trace"
fi
echo "[PROFILE] Relay trace prepared: $dest_trace (source: $trace_file)"
}
# ------------------------------
# Eval (lm-eval-harness) helpers
# ------------------------------
_install_lm_eval_deps() {
# torchvision causes circular imports in ATOM; TRT-LLM/SGLang need it at module level.
if [[ "${IMAGE:-}" == *atom* ]]; then
python3 -m pip uninstall -y torchvision 2>/dev/null || true
fi
python3 -m pip install -q --no-cache-dir --break-system-packages "lm-eval[api]" || true
local lm_eval_ref="b315ef3b05176acc9732bb7fdec116abe1ecc476"
if command -v git >/dev/null 2>&1; then
if ! python3 -m pip install -q --no-cache-dir --no-deps --force-reinstall --break-system-packages \
"git+https://github.com/EleutherAI/lm-evaluation-harness.git@${lm_eval_ref}"; then
python3 -m pip install -q --no-cache-dir --no-deps --force-reinstall --break-system-packages \
"https://github.com/EleutherAI/lm-evaluation-harness/archive/${lm_eval_ref}.tar.gz" || true
fi
else
python3 -m pip install -q --no-cache-dir --no-deps --force-reinstall --break-system-packages \
"https://github.com/EleutherAI/lm-evaluation-harness/archive/${lm_eval_ref}.tar.gz" || true
fi
}
_eval_patches_dir() {
cd "$(dirname "${BASH_SOURCE[0]}")/../utils/evals/patches" && pwd
}
_patch_lm_eval() {
local patch_dir
patch_dir="$(mktemp -d)"
cp "$(_eval_patches_dir)/lm_eval_sitecustomize.py" "$patch_dir/sitecustomize.py"
export PYTHONPATH="${patch_dir}${PYTHONPATH:+:${PYTHONPATH}}"
}
get_native_max_context_length() {
local model_path="$1"
# Prefer MODEL_PATH (local model directory) when available, since the
# argument may be a served-model name that is neither a valid HF repo
# ID nor a local path (e.g. "deepseek-r1-fp4" on the B300 cluster).
if [ -n "${MODEL_PATH:-}" ] && [ -d "${MODEL_PATH}" ]; then
model_path="${MODEL_PATH}"
fi
python3 -c "
try:
from transformers import AutoConfig
config = AutoConfig.from_pretrained('${model_path}', trust_remote_code=True)
for attr in ['max_position_embeddings', 'max_sequence_length', 'seq_length', 'n_positions']:
if hasattr(config, attr):
print(getattr(config, attr))
break
else:
print(0)
except Exception:
print(0)
"
}
# Compute the context length for eval-only mode.
# Uses the requested benchmark context capped at the model's native max.
# Sets EVAL_MAX_MODEL_LEN (needed by run_lm_eval).
# Echoes the computed value for scripts to capture.
#
# Usage: local ctx=$(compute_eval_context_length "$MODEL" "${current_ctx}")
compute_eval_context_length() {
local model="$1"
local benchmark_ctx="${2:-0}"
local native_max
native_max=$(get_native_max_context_length "$model")
native_max="${native_max:-0}"
if [ "$benchmark_ctx" -eq 0 ] 2>/dev/null; then
benchmark_ctx="${native_max:-0}"
fi
local eval_ctx=$(( benchmark_ctx * 1 ))
if [ "$native_max" -gt 0 ] 2>/dev/null && [ "$eval_ctx" -gt "$native_max" ]; then
eval_ctx="$native_max"
fi
# If eval_ctx is still 0 (both benchmark_ctx and native_max were 0), fall back
if [ "$eval_ctx" -le 0 ] 2>/dev/null; then
echo "WARN: compute_eval_context_length could not determine context length for $model" >&2
eval_ctx="${MAX_MODEL_LEN:-16384}"
fi
EVAL_MAX_MODEL_LEN="$eval_ctx"
echo "$eval_ctx"
}
# Convenience wrapper: compute eval context from ISL/OSL and export EVAL_MAX_MODEL_LEN.
# Call directly (not in a subshell) so the export persists.
# Scripts then wire $EVAL_MAX_MODEL_LEN into whichever server variable they need.
setup_eval_context() {
EVAL_MAX_MODEL_LEN=$(compute_eval_context_length "$MODEL" "$((ISL + OSL + 256))")
export EVAL_MAX_MODEL_LEN
}
run_lm_eval() {
local port="${PORT:-8888}"
local tasks_dir="${EVAL_TASKS_DIR:-utils/evals/gsm8k.yaml}"
local results_dir="${EVAL_RESULT_DIR:-$(mktemp -d /tmp/eval_out-XXXXXX)}"
local eval_context_len="${EVAL_MAX_MODEL_LEN:-16384}"
local temperature=0
local top_p=1
local concurrent_requests="${EVAL_CONCURRENT_REQUESTS:-${CONC:-64}}"
# SWE-bench adds a repo-local task YAML, so pass its task directory via
# --include_path. Full-dataset runs remain the default; --limit is passed
# only when EVAL_LIMIT explicitly requests a smaller smoke-test slice.
local eval_limit="${EVAL_LIMIT:-}"
local include_path="${EVAL_INCLUDE_PATH:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--port|--task|--results-dir|--gen-max-tokens|--temperature|--top-p)
if [[ $# -lt 2 || -z "${2:-}" || "${2:-}" == --* ]]; then
echo "ERROR: $1 requires a value" >&2
return 2
fi
case "$1" in
--port) port="$2" ;;
--task) tasks_dir="$2" ;;
--results-dir) results_dir="$2" ;;
--gen-max-tokens) eval_context_len="$2" ;;
--temperature) temperature="$2" ;;
--top-p) top_p="$2" ;;
esac
shift 2
;;
*)
echo "Unknown parameter: $1" >&2
return 2
;;
esac
done
# Serving images may use a different WORKDIR.
local _repo_root
_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"