From a7e0b9e758ad9c7e4fea62b2dd34617d947dd14b Mon Sep 17 00:00:00 2001 From: adibarra <93070681+adibarra@users.noreply.github.com> Date: Mon, 6 Jul 2026 22:58:26 -0500 Subject: [PATCH] Add context parallel config options --- .github/workflows/benchmark-tmpl.yml | 20 +++++- .github/workflows/e2e-tests.yml | 6 ++ .github/workflows/profile.yml | 17 +++-- .github/workflows/run-sweep.yml | 8 +++ benchmarks/benchmark_lib.sh | 2 + .../single_node/agentic/dsv4_fp4_b200_vllm.sh | 20 +++++- .../single_node/agentic/dsv4_fp4_b300_vllm.sh | 20 +++++- configs/CONFIGS.md | 9 ++- runners/launch_b200-cw.sh | 4 +- runners/launch_b200-dgxc.sh | 4 +- runners/launch_b200-nb.sh | 4 +- runners/launch_b300-nv.sh | 4 +- runners/launch_h100-cr.sh | 10 ++- runners/launch_h100-cw.sh | 4 +- runners/launch_h100-dgxc-slurm.sh | 4 +- runners/launch_h200-cw.sh | 4 +- runners/launch_h200-dgxc-slurm.sh | 4 +- runners/launch_h200-nb.sh | 4 +- runners/launch_mi300x-amds.sh | 4 +- runners/launch_mi325x-amds.sh | 4 +- runners/launch_mi355x-amds.sh | 4 +- .../aggregation/process_agentic_result.py | 8 ++- .../test_process_agentic_result.py | 30 +++++--- utils/matrix_logic/generate_sweep_configs.py | 37 ++++++++-- .../test_generate_sweep_configs.py | 72 ++++++++++++++++++- utils/matrix_logic/test_validation.py | 13 ++++ utils/matrix_logic/validation.py | 56 +++++++++++++-- utils/process_result.py | 13 +++- utils/test_process_result.py | 12 ++-- .../test_validate_reusable_sweep_artifacts.py | 20 ++++++ utils/validate_reusable_sweep_artifacts.py | 6 ++ 31 files changed, 368 insertions(+), 59 deletions(-) diff --git a/.github/workflows/benchmark-tmpl.yml b/.github/workflows/benchmark-tmpl.yml index 4cfa9d7a78..6c0333ceff 100644 --- a/.github/workflows/benchmark-tmpl.yml +++ b/.github/workflows/benchmark-tmpl.yml @@ -32,6 +32,15 @@ on: tp: required: true type: string + dcp-size: + required: false + type: string + default: '1' + pcp-size: + required: false + type: string + default: '1' + ep: required: true type: string @@ -105,6 +114,8 @@ env: FRAMEWORK: ${{ inputs.framework }} PRECISION: ${{ inputs.precision }} TP: ${{ inputs.tp }} + DCP_SIZE: ${{ inputs.dcp-size }} + PCP_SIZE: ${{ inputs.pcp-size }} EP_SIZE: ${{ inputs.ep }} DP_ATTENTION: ${{ inputs.dp-attn }} CONC: ${{ inputs.conc }} @@ -134,7 +145,7 @@ jobs: timeout-minutes: 500 name: >- ${{ inputs.model-prefix }} ${{ inputs.precision }} ${{ inputs.runner }} ${{ inputs.framework == 'sglang' && 'sgl' || inputs.framework == 'dynamo-sglang' && 'dyn-sgl' || inputs.framework == 'sglang-disagg' && 'sgl-disagg' || inputs.framework }} - TP${{ inputs.tp }}${{ inputs.ep != '' && inputs.ep != '1' && format('/EP{0}', inputs.ep) || '' }}${{ inputs.dp-attn && '/DPA' || '' }} + TP${{ inputs.tp }}/DCP${{ inputs.dcp-size }}/PCP${{ inputs.pcp-size }}${{ inputs.ep != '' && inputs.ep != '1' && format('/EP{0}', inputs.ep) || '' }}${{ inputs.dp-attn && '/DPA' || '' }} ${{ inputs.spec-decoding != 'none' && inputs.spec-decoding || '' }} ${{ inputs.kv-offloading != '' && inputs.kv-offloading != 'none' && format('{0} KV offload', inputs.kv-offloading) || '' }} ${{ inputs.kv-offload-backend != '' && inputs.kv-offload-backend != 'none' && inputs.kv-offload-backend != 'default' && inputs.kv-offload-backend || '' }} @@ -175,12 +186,15 @@ jobs: env: RUNNER_NAME: ${{ runner.name }} RUNNER_TYPE: ${{ inputs.runner }} - # Hash uniquely on {EXP_NAME}_{PRECISION}_{FRAMEWORK}_tp{}-ep{}-dpa{}_disagg-{}_spec-{}_conc{}_{runner} - RESULT_FILENAME: ${{ env.EXP_NAME }}_${{ env.PRECISION }}_${{ env.FRAMEWORK }}_tp${{ env.TP }}-ep${{ env.EP_SIZE }}-dpa${{ env.DP_ATTENTION }}_disagg-${{ env.DISAGG }}_spec-${{ env.SPEC_DECODING }}_conc${{ env.CONC }}_${{ runner.name }} + # Hash uniquely on {EXP_NAME}_{PRECISION}_{FRAMEWORK}_tp{}-dcp{}-pcp{}-ep{}-dpa{}_disagg-{}_spec-{}_conc{}_{runner} + RESULT_FILENAME: ${{ env.EXP_NAME }}_${{ env.PRECISION }}_${{ env.FRAMEWORK }}_tp${{ env.TP }}-dcp${{ env.DCP_SIZE }}-pcp${{ env.PCP_SIZE }}-ep${{ env.EP_SIZE }}-dpa${{ env.DP_ATTENTION }}_disagg-${{ env.DISAGG }}_spec-${{ env.SPEC_DECODING }}_conc${{ env.CONC }}_${{ runner.name }} # Suppress per-job eval markdown from being appended to the step summary. # We'll publish a single combined eval table in the collection job instead. GITHUB_STEP_SUMMARY: '' run: | + export GPU_COUNT=$((TP * PCP_SIZE)) + echo "GPU_COUNT=${GPU_COUNT}" >> "$GITHUB_ENV" + # Export RESULT_FILENAME early so it's available for artifact uploads even if cancelled echo "RESULT_FILENAME=${RESULT_FILENAME}" >> $GITHUB_ENV diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index cdb7247b30..d54e2e7357 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -185,6 +185,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} @@ -267,6 +269,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} @@ -297,6 +301,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} diff --git a/.github/workflows/profile.yml b/.github/workflows/profile.yml index 5c66e30127..ba40499d8a 100644 --- a/.github/workflows/profile.yml +++ b/.github/workflows/profile.yml @@ -98,6 +98,9 @@ jobs: fail-fast: false matrix: config: ${{ fromJson(needs.get-jobs.outputs.filtered-matrix) }} + name: >- + ${{ matrix.config.model-prefix }} ${{ matrix.config.precision }} ${{ matrix.config.runner }} ${{ matrix.config.framework }} + TP${{ matrix.config.tp }}/DCP${{ matrix.config.dcp-size }}/PCP${{ matrix.config.pcp-size }} c${{ matrix.config.conc }} runs-on: ${{ matrix.config.runner }} env: EXP_NAME: ${{ matrix.config.exp-name }} @@ -110,6 +113,8 @@ jobs: FRAMEWORK: ${{ matrix.config.framework }} PRECISION: ${{ matrix.config.precision }} TP: ${{ matrix.config.tp }} + DCP_SIZE: ${{ matrix.config.dcp-size }} + PCP_SIZE: ${{ matrix.config.pcp-size }} EP_SIZE: ${{ matrix.config.ep }} DP_ATTENTION: ${{ matrix.config['dp-attn'] }} CONC: ${{ matrix.config.conc }} @@ -159,8 +164,10 @@ jobs: shell: bash run: | set -euo pipefail + export GPU_COUNT=$((TP * PCP_SIZE)) + echo "GPU_COUNT=${GPU_COUNT}" >> "$GITHUB_ENV" ep_val="${EP_SIZE:-1}" - res_name="${EXP_NAME}_${PRECISION}_${FRAMEWORK}_tp${TP}_ep${ep_val}_dpa_${DP_ATTENTION}_conc${CONC}_${RUNNER_NAME}" + res_name="${EXP_NAME}_${PRECISION}_${FRAMEWORK}_tp${TP}-dcp${DCP_SIZE}-pcp${PCP_SIZE}_ep${ep_val}_dpa_${DP_ATTENTION}_conc${CONC}_${RUNNER_NAME}" export RESULT_FILENAME="${res_name}" echo "RESULT_FILENAME=${res_name}" >> "$GITHUB_ENV" @@ -252,7 +259,7 @@ jobs: run: | set -euo pipefail - dest_dir="storage/profiles/${GITHUB_SHA}/${{ matrix.config.runner }}/${{ matrix.config.framework }}/${{ matrix.config['exp-name'] }}_${{ matrix.config.precision }}_tp${{ matrix.config.tp }}_ep${{ matrix.config.ep || 1 }}_conc${{ matrix.config.conc }}" + dest_dir="storage/profiles/${GITHUB_SHA}/${{ matrix.config.runner }}/${{ matrix.config.framework }}/${{ matrix.config['exp-name'] }}_${{ matrix.config.precision }}_tp${{ matrix.config.tp }}_dcp${{ matrix.config.dcp-size }}_pcp${{ matrix.config.pcp-size }}_ep${{ matrix.config.ep || 1 }}_conc${{ matrix.config.conc }}" mkdir -p "$dest_dir" cp "$TRACE_LOCAL" "$dest_dir/trace.json.gz" @@ -260,13 +267,13 @@ jobs: git config user.name "github-actions" git config user.email "github-actions@github.com" git add -A - git commit -m "Add profile: ${GITHUB_SHA} ${{ matrix.config['exp-name'] }} tp${{ matrix.config.tp }} ep${{ matrix.config.ep || 1 }} conc${{ matrix.config.conc }}" || echo "Nothing to commit" + git commit -m "Add profile: ${GITHUB_SHA} ${{ matrix.config['exp-name'] }} tp${{ matrix.config.tp }} dcp${{ matrix.config.dcp-size }} pcp${{ matrix.config.pcp-size }} ep${{ matrix.config.ep || 1 }} conc${{ matrix.config.conc }}" || echo "Nothing to commit" git push STORAGE_SHA="$(git rev-parse HEAD)" popd >/dev/null - export RAW_URL="https://raw.githubusercontent.com/SemiAnalysisAI/InferenceX-trace-storage/${STORAGE_SHA}/profiles/${GITHUB_SHA}/${{ matrix.config.runner }}/${{ matrix.config.framework }}/${{ matrix.config['exp-name'] }}_${{ matrix.config.precision }}_tp${{ matrix.config.tp }}_ep${{ matrix.config.ep || 1 }}_conc${{ matrix.config.conc }}/trace.json.gz" - export TITLE="${{ matrix.config['exp-name'] }}_${{ matrix.config.precision }}_tp${{ matrix.config.tp }}_ep${{ matrix.config.ep || 1 }}_conc${{ matrix.config.conc }}" + export RAW_URL="https://raw.githubusercontent.com/SemiAnalysisAI/InferenceX-trace-storage/${STORAGE_SHA}/profiles/${GITHUB_SHA}/${{ matrix.config.runner }}/${{ matrix.config.framework }}/${{ matrix.config['exp-name'] }}_${{ matrix.config.precision }}_tp${{ matrix.config.tp }}_dcp${{ matrix.config.dcp-size }}_pcp${{ matrix.config.pcp-size }}_ep${{ matrix.config.ep || 1 }}_conc${{ matrix.config.conc }}/trace.json.gz" + export TITLE="${{ matrix.config['exp-name'] }}_${{ matrix.config.precision }}_tp${{ matrix.config.tp }}_dcp${{ matrix.config.dcp-size }}_pcp${{ matrix.config.pcp-size }}_ep${{ matrix.config.ep || 1 }}_conc${{ matrix.config.conc }}" enc_src="$(python3 -c 'import os,urllib.parse; print(urllib.parse.quote(os.environ["RAW_URL"], safe=""))')" enc_title="$(python3 -c 'import os,urllib.parse; print(urllib.parse.quote(os.environ["TITLE"], safe=""))')" diff --git a/.github/workflows/run-sweep.yml b/.github/workflows/run-sweep.yml index 99b632c33d..fe77ba1010 100644 --- a/.github/workflows/run-sweep.yml +++ b/.github/workflows/run-sweep.yml @@ -331,6 +331,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} @@ -434,6 +436,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} @@ -487,6 +491,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} @@ -582,6 +588,8 @@ jobs: framework: ${{ matrix.config.framework }} precision: ${{ matrix.config.precision }} tp: ${{ matrix.config.tp }} + dcp-size: ${{ matrix.config.dcp-size }} + pcp-size: ${{ matrix.config.pcp-size }} ep: ${{ matrix.config.ep }} dp-attn: ${{ matrix.config.dp-attn }} conc: ${{ matrix.config.conc }} diff --git a/benchmarks/benchmark_lib.sh b/benchmarks/benchmark_lib.sh index 60602ee103..4ac37d7d81 100644 --- a/benchmarks/benchmark_lib.sh +++ b/benchmarks/benchmark_lib.sh @@ -1074,6 +1074,8 @@ append_lm_eval_summary() { "precision": "${prec:-unknown}", "spec_decoding": "${SPEC_DECODING}", "tp": ${TP:-1}, + "dcp_size": ${DCP_SIZE:-1}, + "pcp_size": ${PCP_SIZE:-1}, "conc": ${metadata_conc}, ${batch_metadata} "ep": ${EP_SIZE:-1}, "dp_attention": ${dp_json}, diff --git a/benchmarks/single_node/agentic/dsv4_fp4_b200_vllm.sh b/benchmarks/single_node/agentic/dsv4_fp4_b200_vllm.sh index fdebbc6f81..d723754ac8 100755 --- a/benchmarks/single_node/agentic/dsv4_fp4_b200_vllm.sh +++ b/benchmarks/single_node/agentic/dsv4_fp4_b200_vllm.sh @@ -27,6 +27,23 @@ source "$(dirname "$0")/../../benchmark_lib.sh" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION +DCP_SIZE="${DCP_SIZE:-1}" +PCP_SIZE="${PCP_SIZE:-1}" +VLLM_CP_ARGS=() +if [ "$DCP_SIZE" -gt 1 ]; then + VLLM_CP_ARGS+=(--decode-context-parallel-size "$DCP_SIZE") +fi +if [ "$PCP_SIZE" -gt 1 ]; then + VLLM_CP_ARGS+=(--prefill-context-parallel-size "$PCP_SIZE") +fi + +GPU_COUNT="${GPU_COUNT:-$((TP * PCP_SIZE))}" +if [[ ! "$GPU_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: GPU_COUNT must be a positive integer, got '$GPU_COUNT'" >&2 + exit 1 +fi +export GPU_COUNT + if [[ -n "${SLURM_JOB_ID:-}" ]]; then echo "JOB $SLURM_JOB_ID running on ${SLURMD_NODENAME:-unknown}" fi @@ -95,7 +112,7 @@ OFFLOAD_ARGS=() if require_agentic_kv_offload_backend mooncake; then # Embedded mode contributes one segment per GPU rank to a shared # distributed store, so pre-divide the aggregate host-memory budget. - PER_RANK_GB=$((TOTAL_CPU_DRAM_GB / TP)) + PER_RANK_GB=$((TOTAL_CPU_DRAM_GB / GPU_COUNT)) MOONCAKE_VERSION=0.3.11.post1 agentic_pip_install --quiet --no-cache-dir --no-deps \ @@ -183,6 +200,7 @@ VLLM_CMD=( --kv-cache-dtype fp8 --block-size 256 "${PARALLEL_ARGS[@]}" + "${VLLM_CP_ARGS[@]}" "${EP_ARGS[@]}" --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE","custom_ops":["all"]}' --attention_config.use_fp4_indexer_cache=True diff --git a/benchmarks/single_node/agentic/dsv4_fp4_b300_vllm.sh b/benchmarks/single_node/agentic/dsv4_fp4_b300_vllm.sh index 531a081bee..74dc2129be 100755 --- a/benchmarks/single_node/agentic/dsv4_fp4_b300_vllm.sh +++ b/benchmarks/single_node/agentic/dsv4_fp4_b300_vllm.sh @@ -26,6 +26,23 @@ source "$(dirname "$0")/../../benchmark_lib.sh" check_env_vars MODEL TP CONC KV_OFFLOADING TOTAL_CPU_DRAM_GB RESULT_DIR DURATION EP_SIZE DP_ATTENTION +DCP_SIZE="${DCP_SIZE:-1}" +PCP_SIZE="${PCP_SIZE:-1}" +VLLM_CP_ARGS=() +if [ "$DCP_SIZE" -gt 1 ]; then + VLLM_CP_ARGS+=(--decode-context-parallel-size "$DCP_SIZE") +fi +if [ "$PCP_SIZE" -gt 1 ]; then + VLLM_CP_ARGS+=(--prefill-context-parallel-size "$PCP_SIZE") +fi + +GPU_COUNT="${GPU_COUNT:-$((TP * PCP_SIZE))}" +if [[ ! "$GPU_COUNT" =~ ^[1-9][0-9]*$ ]]; then + echo "Error: GPU_COUNT must be a positive integer, got '$GPU_COUNT'" >&2 + exit 1 +fi +export GPU_COUNT + if declare -p SLURM_JOB_ID >/dev/null 2>&1 && [ -n "$SLURM_JOB_ID" ]; then SLURM_NODE=unknown if declare -p SLURMD_NODENAME >/dev/null 2>&1 && [ -n "$SLURMD_NODENAME" ]; then @@ -99,7 +116,7 @@ if require_agentic_kv_offload_backend mooncake; then # Mooncake embedded mode contributes one global segment per GPU rank to # a shared distributed store. Pre-divide the aggregate host budget # across those rank-contributed segments. - PER_RANK_GB=$((TOTAL_CPU_DRAM_GB / TP)) + PER_RANK_GB=$((TOTAL_CPU_DRAM_GB / GPU_COUNT)) MOONCAKE_VERSION=0.3.11.post1 agentic_pip_install --quiet --no-cache-dir --no-deps \ @@ -186,6 +203,7 @@ vllm serve "$MODEL_PATH" --served-model-name "$MODEL" \ --kv-cache-dtype fp8 \ --block-size 256 \ "${PARALLEL_ARGS[@]}" \ +"${VLLM_CP_ARGS[@]}" \ "${EP_ARGS[@]}" \ --compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE","custom_ops":["all"]}' \ --attention_config.use_fp4_indexer_cache=True \ diff --git a/configs/CONFIGS.md b/configs/CONFIGS.md index 0ede2e0b77..1f12c76abd 100644 --- a/configs/CONFIGS.md +++ b/configs/CONFIGS.md @@ -18,8 +18,8 @@ entry-name: osl: int search-space: - { tp: int, conc-start: int, conc-end: int } - # Optionally, specify 'ep' (expert-parallelism) and 'dp-attn' (data parallel attention) - - { tp: int, ep: int, dp-attn: bool, conc-start: int, conc-end: int } + # Optionally, specify expert/data-parallel attention and context-parallel sizes + - { tp: int, ep: int, dp-attn: bool, dcp-size: int, pcp-size: int, conc-start: int, conc-end: int } - ... - ... agentic-coding: # optional @@ -86,6 +86,9 @@ The below list describes what each field is: - Note: the step factor between `conc-start` and `conc-end` is 2, so if `conc-start` is 4 and `conc-end` is 128, all concurrencies `4, 8, 16, 32, ..., 128` will be run. - (Optional) `ep`: An integer representing the expert parallelism level that the configuration will be served at. Default is 1 (no expert parallelism) when not specified. - (Optional) `dp-attn`: A boolean representing whether or not to activate data parallel attention for the configuration. Default is false when not specified. + - (Optional) `dcp-size`: Decode context-parallel size. Default is 1. It must be a positive divisor of `tp`; DCP reuses the TP GPUs. + - (Optional) `pcp-size`: Prefill context-parallel size. Default is 1. A single-node job allocates `tp * pcp-size` GPUs. + - `dcp-size` and `pcp-size` are single-node fields. They are not accepted inside multinode `prefill` or `decode` worker blocks. - `agentic-coding`: Agentic trace replay benchmarks using real conversation traces. Each entry must have: - `trace-source`: Identifier for the trace dataset to use. - `search-space`: Same structure as `fixed-seq-len` search-space entries. @@ -96,7 +99,7 @@ input. Notes: - No extra fields besides the ones listed may be specified, or else the benchmarks will fail to run. -- Setting the fields above, particularly `ep` and `dp-attn`, only guarantee that the respective values will be passed as environment variables to the benchmark scripts! Actually using those environment variables is an implementation detail at the level of the benchmark Bash script. +- Setting the fields above only guarantees that their values are passed as environment variables to benchmark scripts (`ep` as `EP_SIZE`, `dp-attn` as `DP_ATTENTION`, `dcp-size` as `DCP_SIZE`, and `pcp-size` as `PCP_SIZE`). Actually using those variables is an implementation detail of the benchmark Bash script. ## Runners diff --git a/runners/launch_b200-cw.sh b/runners/launch_b200-cw.sh index e32b37263a..157aa410fc 100644 --- a/runners/launch_b200-cw.sh +++ b/runners/launch_b200-cw.sh @@ -30,9 +30,11 @@ else CONTAINER_MOUNT_DIR=/workspace fi +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x -JOB_ID=$(salloc --partition=$PARTITION --gres=gpu:b200:$TP --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') +JOB_ID=$(salloc --partition=$PARTITION --gres=gpu:b200:$GPU_COUNT --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') if [ -z "$JOB_ID" ]; then echo "ERROR: salloc failed to allocate a job" diff --git a/runners/launch_b200-dgxc.sh b/runners/launch_b200-dgxc.sh index 7fe590cb32..e08441ebcc 100644 --- a/runners/launch_b200-dgxc.sh +++ b/runners/launch_b200-dgxc.sh @@ -419,8 +419,10 @@ else # and gpu-15 names no longer exist. gpu-2 currently has 10 fully-idle GPU # nodes (all of gpu-2-[0-9]); gpu-1 has 2 drained (gpu-1-4, gpu-1-8). We # land on gpu-2 to avoid drained nodes and skip the per-node excludes. + export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + SALLOC_TIME_LIMIT="${SALLOC_TIME_LIMIT:-480}" - salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT --gres=gpu:$TP --exclusive --mem=0 --time="$SALLOC_TIME_LIMIT" --no-shell --job-name="$RUNNER_NAME" + 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) # DSv4 is also staged on the compute nodes' local RAID. Loading the 806 GB diff --git a/runners/launch_b200-nb.sh b/runners/launch_b200-nb.sh index cb5e800072..7aac1bc04a 100644 --- a/runners/launch_b200-nb.sh +++ b/runners/launch_b200-nb.sh @@ -26,8 +26,10 @@ else CONTAINER_MOUNT_DIR=/workspace fi +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x -srun --partition=$PARTITION --gres=gpu:$TP --exclusive --job-name="$RUNNER_NAME" \ +srun --partition=$PARTITION --gres=gpu:$GPU_COUNT --exclusive --job-name="$RUNNER_NAME" \ --container-image=$IMAGE \ --container-mounts=$GITHUB_WORKSPACE:$CONTAINER_MOUNT_DIR,$HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE \ --no-container-mount-home \ diff --git a/runners/launch_b300-nv.sh b/runners/launch_b300-nv.sh index d8b3e3d86e..6810ee5d85 100644 --- a/runners/launch_b300-nv.sh +++ b/runners/launch_b300-nv.sh @@ -437,8 +437,10 @@ else fi ) + export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + SALLOC_TIME_LIMIT="${SALLOC_TIME_LIMIT:-480}" - salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT -N 1 --gres=gpu:$TP --exclusive --mem=0 --time="$SALLOC_TIME_LIMIT" --no-shell --job-name="$RUNNER_NAME" + salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT -N 1 --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) srun --jobid=$JOB_ID \ diff --git a/runners/launch_h100-cr.sh b/runners/launch_h100-cr.sh index 08a5a967a2..bf86f871c1 100644 --- a/runners/launch_h100-cr.sh +++ b/runners/launch_h100-cr.sh @@ -9,14 +9,18 @@ server_name="bmk-server" # the h200 launchers, which have carried SPEC_SUFFIX since #392). SPEC_SUFFIX=$([[ "$SPEC_DECODING" == "mtp" ]] && printf '_mtp' || printf '') +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" +export CUDA_VISIBLE_DEVICES +CUDA_VISIBLE_DEVICES=$(seq -s, 0 $((GPU_COUNT - 1))) + set -x docker run --rm --network=host --name=$server_name \ ---runtime=nvidia --gpus=all --ipc=host --privileged --shm-size=16g --ulimit memlock=-1 --ulimit stack=67108864 \ +--runtime=nvidia --gpus="$GPU_COUNT" --ipc=host --privileged --shm-size=16g --ulimit memlock=-1 --ulimit stack=67108864 \ -v $HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE \ -v $GITHUB_WORKSPACE:/workspace/ -w /workspace/ \ --e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT=$PORT \ +-e HF_TOKEN -e HF_HUB_CACHE -e MODEL -e TP -e DCP_SIZE -e PCP_SIZE -e GPU_COUNT -e CONC -e MAX_MODEL_LEN -e ISL -e OSL -e RUN_EVAL -e EVAL_ONLY -e RUNNER_TYPE -e RESULT_FILENAME -e RANDOM_RANGE_RATIO -e PORT=$PORT \ -e PROFILE -e SGLANG_TORCH_PROFILER_DIR -e VLLM_TORCH_PROFILER_DIR -e VLLM_RPC_TIMEOUT \ --e PYTHONPYCACHEPREFIX=/tmp/pycache/ -e TORCH_CUDA_ARCH_LIST="9.0" -e CUDA_DEVICE_ORDER=PCI_BUS_ID -e CUDA_VISIBLE_DEVICES="0,1,2,3,4,5,6,7" \ +-e PYTHONPYCACHEPREFIX=/tmp/pycache/ -e TORCH_CUDA_ARCH_LIST="9.0" -e CUDA_DEVICE_ORDER=PCI_BUS_ID -e CUDA_VISIBLE_DEVICES \ --entrypoint=/bin/bash \ $IMAGE \ benchmarks/single_node/${SCENARIO_SUBDIR}"${EXP_NAME%%_*}_${PRECISION}_h100${SPEC_SUFFIX}.sh" diff --git a/runners/launch_h100-cw.sh b/runners/launch_h100-cw.sh index db9ba99b6c..53d6987ff5 100644 --- a/runners/launch_h100-cw.sh +++ b/runners/launch_h100-cw.sh @@ -9,9 +9,11 @@ LOCK_FILE="${SQUASH_FILE}.lock" # the h200 launchers, which have carried SPEC_SUFFIX since #392). SPEC_SUFFIX=$([[ "$SPEC_DECODING" == "mtp" ]] && printf '_mtp' || printf '') +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x -JOB_ID=$(salloc --partition=$PARTITION --gres=gpu:h100:$TP --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') +JOB_ID=$(salloc --partition=$PARTITION --gres=gpu:h100:$GPU_COUNT --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') if [ -z "$JOB_ID" ]; then echo "ERROR: salloc failed to allocate a job" diff --git a/runners/launch_h100-dgxc-slurm.sh b/runners/launch_h100-dgxc-slurm.sh index 442734a809..1334c95542 100644 --- a/runners/launch_h100-dgxc-slurm.sh +++ b/runners/launch_h100-dgxc-slurm.sh @@ -287,7 +287,9 @@ else SQUASH_FILE="/mnt/nfs/lustre/containers/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g').sqsh" LOCK_FILE="${SQUASH_FILE}.lock" - salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT --gres=gpu:$TP --exclusive --time=180 --no-shell --job-name="$RUNNER_NAME" + export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + + salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT --gres=gpu:$GPU_COUNT --exclusive --time=180 --no-shell --job-name="$RUNNER_NAME" JOB_ID=$(squeue --name="$RUNNER_NAME" -u "$USER" -h -o %A | head -n1) if [[ -z "$JOB_ID" ]]; then echo "ERROR: failed to resolve H100 Slurm allocation" >&2 diff --git a/runners/launch_h200-cw.sh b/runners/launch_h200-cw.sh index 6847214974..c850277064 100644 --- a/runners/launch_h200-cw.sh +++ b/runners/launch_h200-cw.sh @@ -12,9 +12,11 @@ PARTITION="h200" SQUASH_FILE="/mnt/vast/gharunner/squash/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g').sqsh" LOCK_FILE="${SQUASH_FILE}.lock" +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x -JOB_ID=$(salloc --partition=$PARTITION --gres=gpu:h200:$TP --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') +JOB_ID=$(salloc --partition=$PARTITION --gres=gpu:h200:$GPU_COUNT --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') if [ -z "$JOB_ID" ]; then echo "ERROR: salloc failed to allocate a job" diff --git a/runners/launch_h200-dgxc-slurm.sh b/runners/launch_h200-dgxc-slurm.sh index 9f9f988015..00c6cc4977 100755 --- a/runners/launch_h200-dgxc-slurm.sh +++ b/runners/launch_h200-dgxc-slurm.sh @@ -280,7 +280,9 @@ else DOCKER_IMAGE=$(echo "$IMAGE" | sed 's/#/\//g') LOCK_FILE="${SQUASH_FILE}.lock" - salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT --gres=gpu:$TP --exclusive --time=180 --no-shell --job-name="$RUNNER_NAME" + export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + + salloc --partition=$SLURM_PARTITION --account=$SLURM_ACCOUNT --gres=gpu:$GPU_COUNT --exclusive --time=180 --no-shell --job-name="$RUNNER_NAME" JOB_ID=$(squeue --name="$RUNNER_NAME" -u "$USER" -h -o %A | head -n1) if [[ -z "$JOB_ID" ]]; then echo "ERROR: failed to resolve H200 Slurm allocation" >&2 diff --git a/runners/launch_h200-nb.sh b/runners/launch_h200-nb.sh index 23d8d816b4..e8f5d43392 100644 --- a/runners/launch_h200-nb.sh +++ b/runners/launch_h200-nb.sh @@ -10,8 +10,10 @@ SPEC_SUFFIX=$([[ "$SPEC_DECODING" == "mtp" ]] && printf '_mtp' || printf '') PARTITION="main" +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x -srun --partition=$PARTITION --gres=gpu:$TP --exclusive --job-name="$RUNNER_NAME" \ +srun --partition=$PARTITION --gres=gpu:$GPU_COUNT --exclusive --job-name="$RUNNER_NAME" \ --container-image=$IMAGE \ --container-name=$(echo "$IMAGE" | sed 's/[\/:@#]/_/g')-${USER} \ --container-mounts=$GITHUB_WORKSPACE:/workspace/,$HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE,$AIPERF_MMAP_CACHE_HOST_PATH:/aiperf_mmap_cache \ diff --git a/runners/launch_mi300x-amds.sh b/runners/launch_mi300x-amds.sh index dabcef6197..fdd03889a0 100644 --- a/runners/launch_mi300x-amds.sh +++ b/runners/launch_mi300x-amds.sh @@ -12,12 +12,14 @@ LOCK_FILE="${SQUASH_FILE}.lock" # the h200 launchers, which have carried SPEC_SUFFIX since #392). SPEC_SUFFIX=$([[ "$SPEC_DECODING" == "mtp" ]] && printf '_mtp' || printf '') +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x # Exclude known-bad nodes; let Slurm pick from anything else: # chi-mi300x-049: persistent /nvme_home disk-full # chi-mi300x-121: provisioning incomplete; missing /raid and Enroot storage -JOB_ID=$(set +o pipefail; salloc --partition=$PARTITION --exclude=chi-mi300x-049,chi-mi300x-121 --gres=gpu:$TP --cpus-per-task=256 --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') +JOB_ID=$(set +o pipefail; salloc --partition=$PARTITION --exclude=chi-mi300x-049,chi-mi300x-121 --gres=gpu:$GPU_COUNT --cpus-per-task=256 --time=180 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') if [ -z "$JOB_ID" ]; then echo "ERROR: salloc failed to allocate a job" diff --git a/runners/launch_mi325x-amds.sh b/runners/launch_mi325x-amds.sh index e1f8527157..a2cad5d496 100644 --- a/runners/launch_mi325x-amds.sh +++ b/runners/launch_mi325x-amds.sh @@ -11,9 +11,11 @@ LOCK_FILE="${SQUASH_FILE}.lock" # the h200 launchers, which have carried SPEC_SUFFIX since #392). SPEC_SUFFIX=$([[ "${SPEC_DECODING:-}" == "mtp" ]] && printf '_mtp' || printf '') +export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x -JOB_ID=$(set +o pipefail; salloc --partition=$PARTITION --gres=gpu:$TP --cpus-per-task=256 --time=480 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') +JOB_ID=$(set +o pipefail; salloc --partition=$PARTITION --gres=gpu:$GPU_COUNT --cpus-per-task=256 --time=480 --no-shell --job-name="$RUNNER_NAME" 2>&1 | tee /dev/stderr | grep -oP 'Granted job allocation \K[0-9]+') if [ -z "$JOB_ID" ]; then echo "ERROR: salloc failed to allocate a job" >&2 diff --git a/runners/launch_mi355x-amds.sh b/runners/launch_mi355x-amds.sh index acfd4912a4..8cb92b7a16 100644 --- a/runners/launch_mi355x-amds.sh +++ b/runners/launch_mi355x-amds.sh @@ -205,12 +205,14 @@ else SQUASH_FILE="/var/lib/squash/$(echo "$IMAGE" | sed 's/[\/:@#]/_/g').sqsh" LOCK_FILE="${SQUASH_FILE}.lock" + export GPU_COUNT="${GPU_COUNT:-${TP:?TP must be set}}" + set -x # Exclude known-bad mi355x compute nodes (KLAUD_DEBUG §5.1 / §5.2): # mia1-p01-g09: pyxis broken (persistently fails to create container filesystem) # mia1-p01-g11: docker.sock permissions denied (cluster-cleanup step fails) # Both have been root-caused via #1431/#1432/#1440/#1441/#1443 sweep failures. - salloc --partition=$PARTITION --exclude=mia1-p01-g09,mia1-p01-g11 --gres=gpu:$TP --exclusive --cpus-per-task=128 --time=500 --no-shell --job-name="$RUNNER_NAME" + salloc --partition=$PARTITION --exclude=mia1-p01-g09,mia1-p01-g11 --gres=gpu:$GPU_COUNT --exclusive --cpus-per-task=128 --time=500 --no-shell --job-name="$RUNNER_NAME" JOB_ID=$(squeue --name="$RUNNER_NAME" -h -o %A | head -n1) srun --jobid=$JOB_ID bash -c "docker stop \$(docker ps -a -q)" diff --git a/utils/agentic/aggregation/process_agentic_result.py b/utils/agentic/aggregation/process_agentic_result.py index d87f452a86..ff685e2c75 100644 --- a/utils/agentic/aggregation/process_agentic_result.py +++ b/utils/agentic/aggregation/process_agentic_result.py @@ -53,11 +53,15 @@ def _gpu_shape() -> tuple[dict[str, Any], int, int, int, str]: tp = env_int("TP", 1) ep = env_int("EP_SIZE", 1) dp_attention = os.environ.get("DP_ATTENTION", "false") - num_gpus = tp fields: dict[str, Any] = {} if not is_multinode: - return fields, num_gpus, tp, ep, dp_attention + dcp_size = env_int("DCP_SIZE", 1) + pcp_size = env_int("PCP_SIZE", 1) + if dcp_size <= 0 or pcp_size <= 0: + raise SystemExit("DCP_SIZE and PCP_SIZE must be positive integers.") + fields.update({"dcp_size": dcp_size, "pcp_size": pcp_size}) + return fields, tp * pcp_size, tp, ep, dp_attention prefill_num_workers = env_int("PREFILL_NUM_WORKERS") prefill_tp = env_int("PREFILL_TP") diff --git a/utils/agentic/aggregation/test_process_agentic_result.py b/utils/agentic/aggregation/test_process_agentic_result.py index 37a10ffc1e..70bd16a172 100644 --- a/utils/agentic/aggregation/test_process_agentic_result.py +++ b/utils/agentic/aggregation/test_process_agentic_result.py @@ -45,6 +45,8 @@ "scenario_type", "is_multinode", "tp", + "dcp_size", + "pcp_size", "ep", "dp_attention", "kv_offloading", @@ -321,6 +323,8 @@ def _run_processor( "FRAMEWORK": "vllm", "PRECISION": "fp4", "TP": "4", + "DCP_SIZE": "1", + "PCP_SIZE": "1", "EP_SIZE": "1", "DP_ATTENTION": "false", "CONC": "8", @@ -434,14 +438,24 @@ def test_processor_derives_interactivity_from_matching_itl_percentile( def test_processor_throughput_per_gpu(tmp_path: Path): result_dir = _write_fixture(tmp_path) output_dir = tmp_path / "out" - agg = _run_processor(result_dir, output_dir) - per_gpu = agg["request_metrics"]["throughput"]["per_gpu"] - assert per_gpu["total_tput_tps"] > 0 - assert per_gpu["input_tput_tps"] > 0 - assert per_gpu["output_tput_tps"] > 0 - assert "tput_per_gpu" not in agg - assert "input_tput_per_gpu" not in agg - assert "output_tput_per_gpu" not in agg + agg = _run_processor( + result_dir, + output_dir, + env_overrides={"TP": "4", "DCP_SIZE": "2", "PCP_SIZE": "2"}, + ) + throughput = agg["request_metrics"]["throughput"] + per_gpu = throughput["per_gpu"] + assert agg["dcp_size"] == 2 + assert agg["pcp_size"] == 2 + assert per_gpu["total_tput_tps"] == pytest.approx( + throughput["total"]["tokens_per_second"] / 8 + ) + assert per_gpu["input_tput_tps"] == pytest.approx( + throughput["input"]["tokens_per_second"] / 8 + ) + assert per_gpu["output_tput_tps"] == pytest.approx( + throughput["output"]["tokens_per_second"] / 8 + ) def test_processor_surfaces_allocated_cpu_dram(tmp_path: Path): diff --git a/utils/matrix_logic/generate_sweep_configs.py b/utils/matrix_logic/generate_sweep_configs.py index 545eb9f969..62f6ef469c 100644 --- a/utils/matrix_logic/generate_sweep_configs.py +++ b/utils/matrix_logic/generate_sweep_configs.py @@ -81,6 +81,14 @@ def runner_gpus_per_node(runner: str, runner_data: dict) -> int: return runner_hardware_int(runner, runner_data, Fields.GPUS_PER_NODE.value) +def effective_gpu_count(benchmark: dict) -> int: + """Return GPUs used by a single-node TP/PCP topology.""" + return ( + benchmark[Fields.TP.value] + * benchmark.get(Fields.PCP_SIZE.value, 1) + ) + + def agentic_dram_offload_gb( agentic_config: dict, benchmark: dict, runner: str, runner_data: dict ) -> int: @@ -95,14 +103,17 @@ def agentic_dram_offload_gb( ) utilization = Decimal(str(agentic_config[Fields.DRAM_UTILIZATION.value])) gpus_per_node = runner_gpus_per_node(runner, runner_data) - tp = benchmark[Fields.TP.value] - if tp > gpus_per_node: + gpu_count = effective_gpu_count(benchmark) + if gpu_count > gpus_per_node: raise ValueError( - f"tp={tp} exceeds {Fields.GPUS_PER_NODE.value}={gpus_per_node} " - f"for runner '{runner}'" + f"tp={benchmark[Fields.TP.value]} with " + f"{Fields.PCP_SIZE.value}={benchmark.get(Fields.PCP_SIZE.value, 1)} " + f"requires {gpu_count} GPUs and exceeds " + f"{Fields.GPUS_PER_NODE.value}={gpus_per_node} for runner '{runner}'" ) proportional_bytes = ( - Decimal(available_mib) * BYTES_PER_MIB * utilization * tp / gpus_per_node + Decimal(available_mib) * BYTES_PER_MIB * utilization + * gpu_count / gpus_per_node ) return int(proportional_bytes / BYTES_PER_GB) @@ -463,6 +474,8 @@ def generate_full_sweep(args, all_config_data, runner_data): else: # Single-node configuration tp = bmk[Fields.TP.value] + dcp_size = bmk.get(Fields.DCP_SIZE.value, 1) + pcp_size = bmk.get(Fields.PCP_SIZE.value, 1) ep = bmk.get(Fields.EP.value) dp_attn = bmk.get(Fields.DP_ATTN.value) spec_decoding = bmk.get(Fields.SPEC_DECODING.value, "none") @@ -555,6 +568,8 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.ISL.value: isl, Fields.OSL.value: osl, Fields.TP.value: tp, + Fields.DCP_SIZE.value: dcp_size, + Fields.PCP_SIZE.value: pcp_size, Fields.CONC.value: conc, Fields.MAX_MODEL_LEN.value: isl + osl + 256, Fields.EP.value: 1, # Default @@ -593,6 +608,8 @@ def generate_full_sweep(args, all_config_data, runner_data): kv_offload_backend = None else: tp = bmk[Fields.TP.value] + dcp_size = bmk.get(Fields.DCP_SIZE.value, 1) + pcp_size = bmk.get(Fields.PCP_SIZE.value, 1) ep = bmk.get(Fields.EP.value) dp_attn = bmk.get(Fields.DP_ATTN.value) kv_offloading = bmk[Fields.KV_OFFLOADING.value] @@ -667,6 +684,8 @@ def generate_full_sweep(args, all_config_data, runner_data): Fields.FRAMEWORK.value: framework, Fields.RUNNER.value: runner_value, Fields.TP.value: tp, + Fields.DCP_SIZE.value: dcp_size, + Fields.PCP_SIZE.value: pcp_size, Fields.EP.value: ep if ep is not None else 1, Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, Fields.CONC.value: conc, @@ -801,6 +820,8 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): else: # Single-node config tp = bmk[Fields.TP.value] + dcp_size = bmk.get(Fields.DCP_SIZE.value, 1) + pcp_size = bmk.get(Fields.PCP_SIZE.value, 1) ep = bmk.get(Fields.EP.value) dp_attn = bmk.get(Fields.DP_ATTN.value) spec_decoding = bmk.get(Fields.SPEC_DECODING.value, "none") @@ -840,6 +861,8 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.ISL.value: isl, Fields.OSL.value: osl, Fields.TP.value: tp, + Fields.DCP_SIZE.value: dcp_size, + Fields.PCP_SIZE.value: pcp_size, Fields.CONC.value: conc, Fields.MAX_MODEL_LEN.value: isl + osl + 256, Fields.EP.value: ep if ep is not None else 1, @@ -866,6 +889,8 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): kv_offload_backend = None else: tp = bmk[Fields.TP.value] + dcp_size = bmk.get(Fields.DCP_SIZE.value, 1) + pcp_size = bmk.get(Fields.PCP_SIZE.value, 1) ep = bmk.get(Fields.EP.value) dp_attn = bmk.get(Fields.DP_ATTN.value) kv_offloading = bmk[Fields.KV_OFFLOADING.value] @@ -933,6 +958,8 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None): Fields.FRAMEWORK.value: framework, Fields.RUNNER.value: runner_value, Fields.TP.value: tp, + Fields.DCP_SIZE.value: dcp_size, + Fields.PCP_SIZE.value: pcp_size, Fields.EP.value: ep if ep is not None else 1, Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False, Fields.CONC.value: conc, diff --git a/utils/matrix_logic/test_generate_sweep_configs.py b/utils/matrix_logic/test_generate_sweep_configs.py index 5a78dedfef..417d6f696a 100644 --- a/utils/matrix_logic/test_generate_sweep_configs.py +++ b/utils/matrix_logic/test_generate_sweep_configs.py @@ -672,6 +672,21 @@ def test_matrix_entry_structure(self, sample_single_node_config, sample_runner_c assert entry["tp"] == 8 assert "exp-name" in entry assert "max-model-len" in entry + assert (entry["dcp-size"], entry["pcp-size"]) == (1, 1) + + explicit_config = copy.deepcopy(sample_single_node_config) + for seq_config in explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"]: + for search_entry in seq_config["search-space"]: + search_entry.update({"dcp-size": 2, "pcp-size": 2}) + explicit_result = generate_full_sweep( + full_sweep_args_single_node, + explicit_config, + sample_runner_config, + ) + assert { + (row["dcp-size"], row["pcp-size"]) + for row in explicit_result + } == {(2, 2)} def test_filter_by_model_prefix(self, sample_single_node_config, sample_runner_config, full_sweep_args_single_node): """Filter by model prefix should work.""" @@ -1833,6 +1848,38 @@ def full_sweep_args_both(): class TestGenerateTestConfigSweep: """Tests for exact config-key sweep generation.""" + def test_single_node_context_parallel_fields_are_generated( + self, + sample_single_node_config, + sample_runner_config, + ): + args = argparse.Namespace( + config_keys=["dsr1-fp8-mi300x-sglang"], + seq_lens=["1k1k"], + conc=[4], + runner_node_filter=None, + ) + + default_result = generate_test_config_sweep( + args, sample_single_node_config, sample_runner_config + ) + assert [ + (row["dcp-size"], row["pcp-size"]) + for row in default_result + ] == [(1, 1)] + + explicit_config = copy.deepcopy(sample_single_node_config) + explicit_config["dsr1-fp8-mi300x-sglang"]["scenarios"]["fixed-seq-len"][0]["search-space"][0].update( + {"dcp-size": 2, "pcp-size": 2} + ) + explicit_result = generate_test_config_sweep( + args, explicit_config, sample_runner_config + ) + assert [ + (row["dcp-size"], row["pcp-size"]) + for row in explicit_result + ] == [(2, 2)] + def test_runner_node_filter_expands_config_runner(self, sample_multinode_config, sample_runner_config): """test-config should allow targeting one concrete runner node.""" args = argparse.Namespace( @@ -1933,6 +1980,22 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config): "kv-offload-backend": "native", "conc-list": [32], }, + { + "tp": 4, + "dcp-size": 2, + "pcp-size": 1, + "kv-offloading": "dram", + "kv-offload-backend": "native", + "conc-list": [32], + }, + { + "tp": 4, + "dcp-size": 1, + "pcp-size": 2, + "kv-offloading": "dram", + "kv-offload-backend": "native", + "conc-list": [32], + }, ], }], }, @@ -1948,9 +2011,12 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config): result = generate_test_config_sweep(args, config, sample_runner_config) - budgets = {entry["tp"]: entry["total-cpu-dram-gb"] for entry in result} - assert budgets == {4: 1199} - assert result[0]["duration"] == 3600 + budgets = { + (entry["dcp-size"], entry["pcp-size"]): entry["total-cpu-dram-gb"] + for entry in result + } + assert budgets == {(1, 1): 1199, (2, 1): 1199, (1, 2): 2399} + assert all(entry["duration"] == 3600 for entry in result) def test_agentic_node_dram_rejects_tp_above_runner_gpus(self, sample_runner_config): config = { diff --git a/utils/matrix_logic/test_validation.py b/utils/matrix_logic/test_validation.py index b60c99f6f3..a51ecbb8a0 100644 --- a/utils/matrix_logic/test_validation.py +++ b/utils/matrix_logic/test_validation.py @@ -41,6 +41,8 @@ def valid_single_node_matrix_entry(): "isl": 1024, "osl": 1024, "tp": 8, + "dcp-size": 1, + "pcp-size": 1, "ep": 1, "dp-attn": False, "conc": 4, @@ -346,6 +348,8 @@ def test_arbitrary_backend_is_valid_for_single_node_agentic_entry(self): "framework": "vllm", "runner": "cluster:b200-dgxc", "tp": 8, + "dcp-size": 1, + "pcp-size": 1, "ep": 1, "dp-attn": False, "conc": 1, @@ -592,6 +596,15 @@ def test_valid_with_conc_list(self): }) assert entry.conc_list == [4, 8, 16, 32, 64, 128] + def test_dcp_size_must_divide_tp(self): + with pytest.raises(Exception, match="must be divisible"): + SingleNodeSearchSpaceEntry(**{ + "tp": 8, + "dcp-size": 3, + "pcp-size": 2, + "conc-list": [4], + }) + def test_cannot_have_both_range_and_list(self): """Cannot specify both conc range and list.""" with pytest.raises(Exception) as exc_info: diff --git a/utils/matrix_logic/validation.py b/utils/matrix_logic/validation.py index 3de1f4d606..bc0485d2bc 100644 --- a/utils/matrix_logic/validation.py +++ b/utils/matrix_logic/validation.py @@ -38,6 +38,8 @@ class Fields(Enum): # Search-space/benchmark fields TP = 'tp' + DCP_SIZE = 'dcp-size' + PCP_SIZE = 'pcp-size' CONC_START = 'conc-start' CONC_END = 'conc-end' CONC_LIST = 'conc-list' @@ -86,6 +88,16 @@ class Fields(Enum): """ +def _validate_single_node_topology(self): + """Validate context-parallel settings shared by single-node schemas.""" + if self.tp % self.dcp_size != 0: + raise ValueError( + f"'{Fields.TP.value}' ({self.tp}) must be divisible by " + f"'{Fields.DCP_SIZE.value}' ({self.dcp_size})" + ) + return self + + class SingleNodeMatrixEntry(BaseModel): """Pydantic model for validating single node matrix entry structure. This validates the input that should be expected to .github/workflows/benchmark-tmpl.yml""" @@ -103,6 +115,8 @@ class SingleNodeMatrixEntry(BaseModel): isl: int osl: int tp: int + dcp_size: int = Field(alias=Fields.DCP_SIZE.value, gt=0, strict=True) + pcp_size: int = Field(alias=Fields.PCP_SIZE.value, gt=0, strict=True) ep: int dp_attn: bool = Field(alias=Fields.DP_ATTN.value) conc: Union[int, List[int]] @@ -112,6 +126,10 @@ class SingleNodeMatrixEntry(BaseModel): run_eval: bool = Field(alias=Fields.RUN_EVAL.value) eval_only: bool = Field(alias=Fields.EVAL_ONLY.value, default=False) + @model_validator(mode='after') + def validate_single_node_topology(self): + return _validate_single_node_topology(self) + class WorkerConfig(BaseModel): """Pydantic model for validating worker configuration in multinode entries.""" @@ -181,6 +199,8 @@ class SingleNodeAgenticMatrixEntry(BaseModel): framework: str runner: str tp: int + dcp_size: int = Field(alias=Fields.DCP_SIZE.value, gt=0, strict=True) + pcp_size: int = Field(alias=Fields.PCP_SIZE.value, gt=0, strict=True) ep: int dp_attn: bool = Field(alias=Fields.DP_ATTN.value) conc: int @@ -199,6 +219,10 @@ class SingleNodeAgenticMatrixEntry(BaseModel): def validate_kv_offload_fields(self): return _validate_kv_offload_fields(self) + @model_validator(mode='after') + def validate_single_node_topology(self): + return _validate_single_node_topology(self) + class MultiNodeAgenticMatrixEntry(BaseModel): """Pydantic model for validating multinode agentic coding matrix entries.""" @@ -351,6 +375,10 @@ class SingleNodeSearchSpaceEntry(BaseModel): model_config = ConfigDict(extra='forbid', populate_by_name=True) tp: int + dcp_size: int = Field( + default=1, alias=Fields.DCP_SIZE.value, gt=0, strict=True) + pcp_size: int = Field( + default=1, alias=Fields.PCP_SIZE.value, gt=0, strict=True) ep: Optional[int] = None spec_decoding: Literal["mtp", "draft_model", "none"] = Field( default="none", alias=Fields.SPEC_DECODING.value) @@ -367,6 +395,10 @@ class SingleNodeSearchSpaceEntry(BaseModel): def validate_conc_fields(self): return _validate_conc_fields(self) + @model_validator(mode='after') + def validate_single_node_topology(self): + return _validate_single_node_topology(self) + class MultiNodeSearchSpaceEntry(BaseModel): """Multinode search space configuration.""" @@ -417,6 +449,10 @@ class AgenticCodingSearchSpaceEntry(BaseModel): model_config = ConfigDict(extra='forbid', populate_by_name=True) tp: Optional[int] = None + dcp_size: int = Field( + default=1, alias=Fields.DCP_SIZE.value, gt=0, strict=True) + pcp_size: int = Field( + default=1, alias=Fields.PCP_SIZE.value, gt=0, strict=True) ep: Optional[int] = None dp_attn: Optional[bool] = Field(default=None, alias=Fields.DP_ATTN.value) spec_decoding: Literal["mtp", "draft_model", "none"] = Field( @@ -452,12 +488,22 @@ def validate_topology_fields(self): valid = has_complete_multinode if not valid: raise ValueError("Agentic search-space entries must specify either tp or both prefill and decode") - if has_single_node and self.kv_offloading is None: - raise ValueError( - f"Single-node agentic search-space entries must specify " - f"{Fields.KV_OFFLOADING.value}" - ) + if has_single_node: + if self.kv_offloading is None: + raise ValueError( + f"Single-node agentic search-space entries must specify " + f"{Fields.KV_OFFLOADING.value}" + ) + _validate_single_node_topology(self) if has_complete_multinode: + if ( + "dcp_size" in self.model_fields_set + or "pcp_size" in self.model_fields_set + ): + raise ValueError( + "Multinode agentic search-space entries cannot specify " + f"'{Fields.DCP_SIZE.value}' or '{Fields.PCP_SIZE.value}'" + ) _validate_worker_hardware_pair(self) return self diff --git a/utils/process_result.py b/utils/process_result.py index c4801edec1..5981103924 100644 --- a/utils/process_result.py +++ b/utils/process_result.py @@ -122,15 +122,22 @@ def get_required_env_vars(required_vars): tp_size = int(single_node_env['TP']) ep_size = int(single_node_env['EP_SIZE']) dp_attention = single_node_env['DP_ATTENTION'] + dcp_size = int(os.environ.get('DCP_SIZE', '1')) + pcp_size = int(os.environ.get('PCP_SIZE', '1')) + if dcp_size <= 0 or pcp_size <= 0: + raise ValueError("DCP_SIZE and PCP_SIZE must be positive integers.") + num_gpus = tp_size * pcp_size single_node_data = { 'is_multinode': False, 'tp': tp_size, + 'dcp_size': dcp_size, + 'pcp_size': pcp_size, 'ep': ep_size, 'dp_attention': dp_attention, - 'tput_per_gpu': float(bmk_result['total_token_throughput']) / tp_size, - 'output_tput_per_gpu': float(bmk_result['output_throughput']) / tp_size, - 'input_tput_per_gpu': (float(bmk_result['total_token_throughput']) - float(bmk_result['output_throughput'])) / tp_size, + 'tput_per_gpu': float(bmk_result['total_token_throughput']) / num_gpus, + 'output_tput_per_gpu': float(bmk_result['output_throughput']) / num_gpus, + 'input_tput_per_gpu': (float(bmk_result['total_token_throughput']) - float(bmk_result['output_throughput'])) / num_gpus, } data = data | single_node_data diff --git a/utils/test_process_result.py b/utils/test_process_result.py index 127e2512f4..9f19fcb689 100644 --- a/utils/test_process_result.py +++ b/utils/test_process_result.py @@ -373,7 +373,7 @@ def test_tpot_to_interactivity_conversion(self, tmp_path, single_node_env_vars): assert output_data["intvty_p99"] == pytest.approx(20.0) def test_throughput_per_gpu_single_node(self, tmp_path, single_node_env_vars): - """Test throughput per GPU calculation for single node.""" + """PCP expands the GPU denominator while DCP remains metadata.""" benchmark_result = { "model_id": "test-model", "max_concurrency": 8, @@ -382,15 +382,17 @@ def test_throughput_per_gpu_single_node(self, tmp_path, single_node_env_vars): } env = single_node_env_vars.copy() - env["TP"] = "4" + env.update({"TP": "4", "DCP_SIZE": "2", "PCP_SIZE": "2"}) result = run_script(tmp_path, env, benchmark_result) assert result.returncode == 0, f"Script failed: {result.stderr}" output_data = json.loads(result.stdout) - assert output_data["tput_per_gpu"] == pytest.approx(2000.0) # 8000 / 4 - assert output_data["output_tput_per_gpu"] == pytest.approx(1500.0) # 6000 / 4 - assert output_data["input_tput_per_gpu"] == pytest.approx(500.0) # (8000 - 6000) / 4 + assert output_data["dcp_size"] == 2 + assert output_data["pcp_size"] == 2 + assert output_data["tput_per_gpu"] == pytest.approx(8000.0 / 8) + assert output_data["output_tput_per_gpu"] == pytest.approx(6000.0 / 8) + assert output_data["input_tput_per_gpu"] == pytest.approx(2000.0 / 8) def test_throughput_per_gpu_multinode(self, tmp_path, multinode_env_vars): """Test throughput per GPU calculation for multinode.""" diff --git a/utils/test_validate_reusable_sweep_artifacts.py b/utils/test_validate_reusable_sweep_artifacts.py index 994295765d..4e0406d66c 100644 --- a/utils/test_validate_reusable_sweep_artifacts.py +++ b/utils/test_validate_reusable_sweep_artifacts.py @@ -6,6 +6,7 @@ from validate_reusable_sweep_artifacts import ( agentic_key, + benchmark_key, dedupe_reran_evals, main, validate_agentic_artifacts, @@ -130,6 +131,8 @@ def fixed_result(conc: int) -> dict: "isl": 1024, "osl": 1024, "tp": 2, + "dcp_size": 1, + "pcp_size": 1, "ep": 1, "dp_attention": False, "conc": conc, @@ -146,6 +149,8 @@ def agentic_result(conc: int = 16) -> dict: "scenario_type": "agentic-coding", "is_multinode": False, "tp": 8, + "dcp_size": 1, + "pcp_size": 1, "ep": 8, "dp_attention": "true", "conc": conc, @@ -153,6 +158,21 @@ def agentic_result(conc: int = 16) -> dict: } +def test_single_node_reusable_keys_normalize_legacy_cp_and_separate_variants() -> None: + cases = ( + ("fixed", benchmark_key, fixed_result(16)), + ("agentic", agentic_key, agentic_result()), + ) + + for name, identity, row in cases: + legacy_row = dict(row) + legacy_row.pop("dcp_size") + legacy_row.pop("pcp_size") + assert identity(legacy_row) == identity(row), name + assert identity({**row, "dcp_size": 2}) != identity(row), name + assert identity({**row, "pcp_size": 2}) != identity(row), name + + def test_multinode_agentic_identity_fields_match() -> None: row = { "hw": "gb200", diff --git a/utils/validate_reusable_sweep_artifacts.py b/utils/validate_reusable_sweep_artifacts.py index 9b14782312..cbd342adb1 100644 --- a/utils/validate_reusable_sweep_artifacts.py +++ b/utils/validate_reusable_sweep_artifacts.py @@ -78,6 +78,8 @@ def benchmark_key(row: dict[str, Any]) -> tuple[Any, ...]: as_int(row.get("isl")), as_int(row.get("osl")), as_int(row.get("tp")), + as_int(row.get("dcp_size", 1), 1), + as_int(row.get("pcp_size", 1), 1), as_int(row.get("ep", 1)), as_bool(row.get("dp_attention", False)), as_int(row.get("conc")), @@ -129,6 +131,8 @@ def agentic_key(row: dict[str, Any]) -> tuple[Any, ...]: row.get("framework"), row.get("precision"), as_int(row.get("tp")), + as_int(row.get("dcp_size", 1), 1), + as_int(row.get("pcp_size", 1), 1), as_int(row.get("ep", 1)), as_bool(row.get("dp_attention", False)), as_int(row.get("conc")), @@ -299,6 +303,8 @@ def eval_key(row: dict[str, Any]) -> tuple[Any, ...]: as_int(row.get("isl", 8192), 8192), as_int(row.get("osl", 1024), 1024), as_int(row.get("tp")), + as_int(row.get("dcp_size", 1), 1), + as_int(row.get("pcp_size", 1), 1), as_int(row.get("ep", 1)), as_bool(row.get("dp_attention", False)), as_int(row.get("conc")),