diff --git a/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh b/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh new file mode 100755 index 0000000000..d2bcc15996 --- /dev/null +++ b/benchmarks/single_node/agentic/qwen3.5_fp4_b200_sglang_mtp.sh @@ -0,0 +1,173 @@ +#!/usr/bin/env bash +set -euo pipefail +set -x + +# AgentX trace replay for Qwen3.5-397B-A17B NVFP4 on B200 with SGLang +# native NEXTN MTP. Throughput uses the committed golden synthetic AL; evals +# retain real target-model verification. + +source "$(dirname "$0")/../../benchmark_lib.sh" + +check_env_vars \ + MODEL TP CONC EP_SIZE KV_OFFLOADING \ + TOTAL_CPU_DRAM_GB RESULT_DIR DURATION + +SCHEDULER_RECV_INTERVAL=${SCHEDULER_RECV_INTERVAL:-10} + +if [[ -n "${SLURM_JOB_ID:-}" ]]; then + echo "JOB $SLURM_JOB_ID running on ${SLURMD_NODENAME:-unknown}" +fi + +if [[ -n "${MODEL_PATH:-}" ]]; then + if [[ ! -d "$MODEL_PATH" || -z "$(ls -A "$MODEL_PATH" 2>/dev/null)" ]]; then + hf download "$MODEL" --local-dir "$MODEL_PATH" + fi +else + hf download "$MODEL" + export MODEL_PATH="$MODEL" +fi +nvidia-smi + +export WEKA_LOADER_OVERRIDE=semianalysis_cc_traces_weka_062126_256k +resolve_trace_source +install_agentic_deps + +SERVER_LOG="$RESULT_DIR/server.log" +mkdir -p "$RESULT_DIR" + +CACHE_ARGS=() +if require_agentic_kv_offload_backend hicache; then + REQUESTED_HICACHE_TOTAL_GB="${HICACHE_TOTAL_CPU_DRAM_GB:-$TOTAL_CPU_DRAM_GB}" + if [ "$REQUESTED_HICACHE_TOTAL_GB" -gt "$TOTAL_CPU_DRAM_GB" ]; then + echo "Error: requested HiCache pool ${REQUESTED_HICACHE_TOTAL_GB} GB exceeds configured capacity ${TOTAL_CPU_DRAM_GB} GB" >&2 + exit 1 + fi + TOTAL_CPU_DRAM_GB="$REQUESTED_HICACHE_TOTAL_GB" + # SGLang applies --hicache-size independently to Qwen's target KV and + # Mamba pools. Native NEXTN also creates a draft KV pool with the same + # slot count; its one attention layer adds 1/15 of the target KV bytes. + # Reserve 1 GB/rank for page alignment and enforce H * 31/15 per rank. + HICACHE_ALIGNMENT_RESERVE_GB=$TP + HICACHE_USABLE_TOTAL_GB=$((TOTAL_CPU_DRAM_GB - HICACHE_ALIGNMENT_RESERVE_GB)) + if [ "$HICACHE_USABLE_TOTAL_GB" -lt 1 ]; then + echo "Error: insufficient DRAM after HiCache alignment reserve" >&2 + exit 1 + fi + MAX_HICACHE_SIZE_GB=$((HICACHE_USABLE_TOTAL_GB * 15 / TP / 31)) + HICACHE_SIZE_GB="${HICACHE_SIZE_GB:-$MAX_HICACHE_SIZE_GB}" + if [ "$HICACHE_SIZE_GB" -lt 1 ] || [ "$HICACHE_SIZE_GB" -gt "$MAX_HICACHE_SIZE_GB" ]; then + echo "Error: HICACHE_SIZE_GB=$HICACHE_SIZE_GB outside 1..$MAX_HICACHE_SIZE_GB" >&2 + exit 1 + fi + PROJECTED_HICACHE_TOTAL_GB=$(((HICACHE_SIZE_GB * TP * 31 + 14) / 15 + HICACHE_ALIGNMENT_RESERVE_GB)) + if [ "$PROJECTED_HICACHE_TOTAL_GB" -gt "$TOTAL_CPU_DRAM_GB" ]; then + echo "Error: projected HiCache use ${PROJECTED_HICACHE_TOTAL_GB} GB exceeds configured capacity ${TOTAL_CPU_DRAM_GB} GB" >&2 + exit 1 + fi + echo "HiCache CPU pools: ${HICACHE_SIZE_GB} GB target + Mamba + 1/15 draft per rank across TP=${TP}; projected node total ${PROJECTED_HICACHE_TOTAL_GB} GB <= ${TOTAL_CPU_DRAM_GB} GB" + CACHE_ARGS=( + --page-size 64 + --enable-hierarchical-cache + --hicache-size "$HICACHE_SIZE_GB" + --hicache-io-backend kernel + --hicache-mem-layout page_first + --hicache-write-policy write_through_selective + ) +fi + +PARALLEL_ARGS=( + --tp "$TP" + --dp 1 + --ep-size "$EP_SIZE" +) + +# TP4 needs parallel tokenization to keep 256k AgentX warmups below the client +# request timeout. Keep TP2 on SGLang's single-worker default: multi-tokenizer +# startup races with the TP2 HiCache shared-memory initialization path. +TOKENIZER_ARGS=() +if [ "$TP" -ge 4 ]; then + TOKENIZER_ARGS=(--tokenizer-worker-num 6) +fi + +# AgentX concurrency counts live session trees rather than individual HTTP +# requests. Leave room for subagent fan-out and avoid spending HBM on graphs +# above the batch sizes that remain useful for this long-context workload. +MAX_RUNNING_REQUESTS=$((2 * CONC)) +CUDA_GRAPH_MAX_BS="$CONC" +[ "$CUDA_GRAPH_MAX_BS" -gt 64 ] && CUDA_GRAPH_MAX_BS=64 + +export TORCH_CUDA_ARCH_LIST="10.0" +export PYTHONNOUSERSITE=1 +export NCCL_NVLS_ENABLE=1 +export SGL_ENABLE_JIT_DEEPGEMM=false +export SGLANG_ENABLE_FLASHINFER_GEMM=true + +if [ "${EVAL_ONLY:-false}" != "true" ]; then + export SGLANG_SIMULATE_ACC_LEN=3.39 + export SGLANG_SIMULATE_ACC_METHOD=match-expected + export SGLANG_SIMULATE_ACC_TOKEN_MODE=real-draft-token +fi + +SGLANG_CMD=( + python3 -m sglang.launch_server + --model-path "$MODEL_PATH" + --served-model-name "$MODEL" + --host 0.0.0.0 + --port "$PORT" + --trust-remote-code + "${PARALLEL_ARGS[@]}" + --enable-symm-mem + --quantization modelopt_fp4 + --fp4-gemm-backend flashinfer_cutlass + --kv-cache-dtype fp8_e4m3 + --mamba-ssm-dtype bfloat16 + --attention-backend trtllm_mha + --moe-runner-backend flashinfer_trtllm + --cuda-graph-max-bs "$CUDA_GRAPH_MAX_BS" + --max-running-requests "$MAX_RUNNING_REQUESTS" + --max-prefill-tokens 16384 + --chunked-prefill-size 16384 + --mem-fraction-static 0.80 + --stream-interval 50 + --scheduler-recv-interval "$SCHEDULER_RECV_INTERVAL" + "${TOKENIZER_ARGS[@]}" + --tokenizer-path "$MODEL" + --reasoning-parser qwen3 + --tool-call-parser qwen3_coder + --speculative-algorithm NEXTN + --speculative-num-steps 3 + --speculative-eagle-topk 1 + --speculative-num-draft-tokens 4 + --enable-metrics + --enable-cache-report + "${CACHE_ARGS[@]}" +) + +printf '%q ' "${SGLANG_CMD[@]}" | tee "$RESULT_DIR/sglang_command.txt" +printf '\n' | tee -a "$RESULT_DIR/sglang_command.txt" +"${SGLANG_CMD[@]}" > "$SERVER_LOG" 2>&1 & +SERVER_PID=$! + +capture_cache_metrics() { + { + echo "=== SGLang cache metrics snapshot $(date --iso-8601=seconds) ===" + curl -fsS "http://localhost:$PORT/metrics" 2>/dev/null \ + | grep -E '^(sglang:(cache_hit_rate|cached_tokens_total|prompt_tokens_total|hicache_host_used_tokens|hicache_host_total_tokens|token_usage|num_requests_running|num_requests_waiting))' \ + || true + echo "============================================================" + } >> "$SERVER_LOG" +} + +wait_for_server_ready --port "$PORT" --server-log "$SERVER_LOG" --server-pid "$SERVER_PID" + +capture_cache_metrics +trap capture_cache_metrics EXIT + +if [ "${EVAL_ONLY:-false}" = "true" ]; then + run_eval --port "$PORT" +else + build_replay_cmd "$RESULT_DIR" + REPLAY_CMD+=" --trace-idle-gap-cap-seconds 300" + REPLAY_CMD+=" --server-metrics http://localhost:$PORT/metrics" + run_agentic_replay_and_write_outputs "$RESULT_DIR" +fi diff --git a/configs/nvidia-master.yaml b/configs/nvidia-master.yaml index a30af59641..32019c9ab4 100644 --- a/configs/nvidia-master.yaml +++ b/configs/nvidia-master.yaml @@ -8586,6 +8586,37 @@ minimaxm3-fp8-h200-vllm-agentic: - { tp: 8, ep: 8, kv-offloading: none, conc-list: [2, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20] } - { tp: 8, ep: 8, kv-offloading: dram, kv-offload-backend: { name: mooncake, version: "0.3.11.post1" }, conc-list: [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 16, 18, 20] } +# Qwen3.5 has 15 full-attention layers with two 256-wide KV heads, so its +# target FP8 KV is 15,360 bytes/token before the native MTP layer. TP4 +# replicates the two KV heads and measured 30.5 KB/token node-wide; TP2 +# measured 15.3 KB/token. Against the 256k trace's 88.8k median and 148.8k +# p75 ISL, TP2 no-offload is already GPU-KV-bound by c18; measured c20 +# HiCache improves both its throughput and latency, so offload takes over +# after c14 rather than repeating dominated no-offload points. TP2 c40 is +# capped to 25 running requests by the available HBM and matches c32 throughput +# with 15-second median TTFT, so the HiCache arm ends at c32. +# TP4 retains 83.9% prefix hits at c64. No-offload c66 and c68 are already +# throughput/latency dominated, while c70 collapses to 6% hits and 96% KV +# usage, so HiCache takes over at c64 and densely covers the boundary through +# c76. At c80 GPU KV reaches 100% with 19-second median TTFT, and c88 falls to +# 10% prefix hits, so the dominated upper tail is excluded. +qwen3.5-fp4-b200-sglang-agentic-mtp: + image: lmsysorg/sglang:v0.5.16-cu130 + model: nvidia/Qwen3.5-397B-A17B-NVFP4 + model-prefix: qwen3.5 + runner: cluster:b200-dgxc + precision: fp4 + framework: sglang + multinode: false + scenarios: + agentic-coding: + - dram-utilization: 0.80 + search-space: + - { tp: 4, ep: 1, spec-decoding: mtp, kv-offloading: none, conc-list: [1, 4, 8, 12, 16, 20, 24, 28, 32, 40, 48, 56, 60, 62, 64] } + - { tp: 4, ep: 1, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: hicache }, conc-list: [64, 66, 68, 70, 72, 76] } + - { tp: 2, ep: 1, spec-decoding: mtp, kv-offloading: none, conc-list: [1, 4, 8, 12, 14] } + - { tp: 2, ep: 1, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: hicache }, conc-list: [16, 18, 20, 22, 24, 28, 32] } + dsv4-fp4-b200-sglang-agentic-hicache: image: lmsysorg/sglang:nightly-dev-cu13-20260707-b4155233 model: deepseek-ai/DeepSeek-V4-Pro diff --git a/perf-changelog.yaml b/perf-changelog.yaml index 6eebd97070..14c9c0be9f 100644 --- a/perf-changelog.yaml +++ b/perf-changelog.yaml @@ -5355,3 +5355,12 @@ - "Apply the accuracy-gated Kimi-K2.5 MXFP4 settings: tuned AITER MXFP4 MoE, fused shared experts, FP8 KV cache, block size 16, 16384 batched tokens, 512 sequences, async scheduling, gpu-memory-utilization 0.85 (headroom for CUDA-graph capture on MI355X), and the AITER BF16 GEMM path" - "Extend the TP4 and TP8 8k1k concurrency sweep from 64 to 128 (1k1k deprecated per #2263)" pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2213 + +- config-keys: + - qwen3.5-fp4-b200-sglang-agentic-mtp + description: + - "Add Qwen3.5-397B-A17B NVFP4 AgentX benchmark on B200 with SGLang native NEXTN MTP" + - "Use the 256k trace dataset and golden synthetic acceptance length 3.39" + - "Pin AIPerf trace idle-gap support and cap per-trace idle gaps at 300 seconds" + - "Image: lmsysorg/sglang:v0.5.16-cu130" + pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2420 diff --git a/runners/launch_b200-dgxc.sh b/runners/launch_b200-dgxc.sh index be68ce073b..9cd8588e38 100644 --- a/runners/launch_b200-dgxc.sh +++ b/runners/launch_b200-dgxc.sh @@ -11,6 +11,8 @@ set -x # portability, but we resolve to pre-staged paths here to avoid repeated # downloading on every dgxc node. Runs for both single-node and multinode # launches. +# Single-node Qwen3.5 FP4 runs probe the allocated node for a complete local +# /raid checkpoint below and use this shared path only as the fallback. if [[ $MODEL_PREFIX == "dsr1" && $PRECISION == "fp4" ]]; then export MODEL_PATH="/scratch/fsw/models/DeepSeek-R1-0528-NVFP4-v2" export SRT_SLURM_MODEL_PREFIX="dsr1" @@ -451,10 +453,6 @@ EOF else SQUASH_FILE="/home/sa-shared/containers/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g').sqsh" - # Point the bench script at the local MODEL_PATH resolved above instead of - # pulling from the HF hub cache. Bench scripts skip `hf download` when - # MODEL is a local path. - export MODEL="$MODEL_PATH" FRAMEWORK_SUFFIX=$([[ "$FRAMEWORK" == "trt" ]] && printf '_trt' || printf '') SPEC_SUFFIX=$([[ "$SPEC_DECODING" == "mtp" ]] && printf '_mtp' || printf '') # Prefer a framework-tagged script (e.g. dsv4_fp4_b200_vllm.sh) so models @@ -488,6 +486,41 @@ else salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT --gres=gpu:$GPU_COUNT --exclusive --mem=0 --time="$SALLOC_TIME_LIMIT" --no-shell --job-name="$RUNNER_NAME" JOB_ID=$(squeue --name="$RUNNER_NAME" -u "$USER" -h -o %A | head -n1) + # Qwen3.5 FP4 may be staged on local NVMe on a DGXC node. + # Resolve this only after Slurm assigns the node; checking on the login + # host would select the wrong filesystem. Validate every shard named by + # the safetensor index so a partial staging directory is never selected. + if [[ "$MODEL_PREFIX" == "qwen3.5" && "$PRECISION" == "fp4" ]]; then + LOCAL_MODEL_PATH="/raid/models/Qwen3.5-397B-A17B-NVFP4" + if srun --jobid="$JOB_ID" --nodes=1 --ntasks=1 \ + python3 - "$LOCAL_MODEL_PATH" <<'PY'; then +import json +from pathlib import Path +import sys + +model_path = Path(sys.argv[1]) +index_path = model_path / "model.safetensors.index.json" +if not (model_path / "config.json").is_file() or not index_path.is_file(): + raise SystemExit(1) + +with index_path.open() as index_file: + shards = set(json.load(index_file).get("weight_map", {}).values()) +if not shards or any(not (model_path / shard).is_file() for shard in shards): + raise SystemExit(1) +PY + MODEL_PATH="$LOCAL_MODEL_PATH" + export MODEL_PATH + echo "Using node-local NVMe checkpoint: $MODEL_PATH" + else + echo "Node-local Qwen3.5 FP4 checkpoint unavailable; using shared checkpoint: $MODEL_PATH" + fi + fi + + # Point the bench script at the resolved local/shared MODEL_PATH instead of + # pulling from the HF hub cache. Bench scripts skip `hf download` when + # MODEL is a local path. + export MODEL="$MODEL_PATH" + # Use flock to serialize concurrent imports to the same squash file # Override ENROOT_CACHE_PATH to avoid permission issues with system-wide cache on worker nodes srun --jobid=$JOB_ID bash -c " diff --git a/utils/aiperf b/utils/aiperf index be758d6218..c12846297f 160000 --- a/utils/aiperf +++ b/utils/aiperf @@ -1 +1 @@ -Subproject commit be758d6218268171e2957fbec9d4f557275bca2d +Subproject commit c12846297fb5b9057cbbd6c8cf55637d8353385d