Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5d79351
feat(agentx): add H100 NVMe KV offload sweep
cquil11 Aug 28, 2026
44d1af3
feat(agentx): add tiered DRAM and NVMe sweep
cquil11 Aug 28, 2026
923f60d
test(agentx): cover tiered KV offload matrices
cquil11 Aug 28, 2026
3ebc733
refactor(agentx): represent tiered offload as tier list
cquil11 Aug 28, 2026
0e41fd1
perf: schedule H100 MiniMax-M3 offload sweep
cquil11 Aug 28, 2026
ef3c36e
fix(agentx): extend H100 allocation time
cquil11 Aug 28, 2026
366d271
fix(h100): clean NVMe before releasing allocation
cquil11 Aug 28, 2026
0545bda
fix(agentx): reject multinode NVMe offload
cquil11 Aug 28, 2026
8de9e47
fix(h100): exclude node with broken NVLink P2P
cquil11 Aug 28, 2026
ae673c5
fix(h100): clean stale vLLM offload memory
cquil11 Aug 28, 2026
d83dd9d
fix(h100): purge stale NVMe offload directories
cquil11 Aug 28, 2026
f436e23
fix(h100): bound offload cleanup steps
cquil11 Aug 28, 2026
e631356
simplify H100 offload lifecycle
cquil11 Aug 28, 2026
fc9433a
test updated cache-source image
cquil11 Aug 28, 2026
48389ac
Merge remote-tracking branch 'origin/main' into codex/h100-minimaxm3-…
cquil11 Aug 28, 2026
1a0793b
bound in-container NVMe cleanup
cquil11 Aug 28, 2026
3cb3641
simplify H100 offload launcher
cquil11 Aug 28, 2026
893cd1f
test tiering timeout image
cquil11 Aug 28, 2026
8a00c0a
clean stale vLLM offload mmap files
cquil11 Aug 31, 2026
a4c2a6c
document stale offload cleanup
cquil11 Aug 31, 2026
b52c639
Merge remote-tracking branch 'origin/main' into codex/h100-minimaxm3-…
cquil11 Aug 31, 2026
cb0fec2
Narrow H100 cache-source validation sweep
cquil11 Sep 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions benchmarks/benchmark_lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,19 @@ require_agentic_kv_offload_backend() {
fi
return 1
;;
dram)
dram|nvme|dram+nvme)
if [[ "${KV_OFFLOAD_BACKEND:-}" != "$expected_backend" ]]; then
echo "Error: expected KV_OFFLOAD_BACKEND=$expected_backend when KV_OFFLOADING=dram, got '${KV_OFFLOAD_BACKEND:-}'" >&2
echo "Error: expected KV_OFFLOAD_BACKEND=$expected_backend when KV_OFFLOADING=$KV_OFFLOADING, 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
if [[ "$KV_OFFLOADING" != "nvme" && ! "${TOTAL_CPU_DRAM_GB:-}" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: $KV_OFFLOADING 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
echo "Error: unsupported KV_OFFLOADING value '$KV_OFFLOADING' (expected one of: none, dram, nvme, dram+nvme)" >&2
exit 1
;;
esac
Expand All @@ -86,18 +86,18 @@ if [[ "$_benchmark_caller" == */agentic/* ||
exit 1
fi
;;
dram)
dram|nvme|dram+nvme)
if [[ -z "${KV_OFFLOAD_BACKEND:-}" || "${KV_OFFLOAD_BACKEND:-}" == "none" ]]; then
echo "Error: KV_OFFLOAD_BACKEND is required when KV_OFFLOADING=dram" >&2
echo "Error: KV_OFFLOAD_BACKEND is required when KV_OFFLOADING=$KV_OFFLOADING" >&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
if [[ "$KV_OFFLOADING" != "nvme" && ! "${TOTAL_CPU_DRAM_GB:-}" =~ ^[1-9][0-9]*$ ]]; then
echo "Error: $KV_OFFLOADING 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
echo "Error: unsupported KV_OFFLOADING value '$KV_OFFLOADING' (expected one of: none, dram, nvme, dram+nvme)" >&2
exit 1
;;
esac
Expand Down
25 changes: 24 additions & 1 deletion benchmarks/single_node/agentic/minimaxm3_fp8_h100_mtp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
set -eo pipefail
set -x

# H100 MiniMax-M3 MXFP8 AgentX with EAGLE3 and optional Mooncake DRAM KV offload.
# H100 MiniMax-M3 MXFP8 AgentX with EAGLE3 and optional DRAM or NVMe KV offload.

source "$(dirname "$0")/../../benchmark_lib.sh"

Expand Down Expand Up @@ -97,6 +97,29 @@ EOF
--kv-transfer-config
'{"kv_connector":"MooncakeStoreConnector","kv_role":"kv_both","kv_connector_extra_config":{"load_async":true}}'
)
elif [ "$KV_OFFLOADING" = "nvme" ]; then
require_agentic_kv_offload_backend vllm-simple
: "${NVME_OFFLOAD_DIR:?NVME_OFFLOAD_DIR must be mounted by the H100 launcher}"
NVME_OFFLOAD_TOTAL_BYTES=8000000000000
NVME_OFFLOAD_PER_RANK_BYTES=$((NVME_OFFLOAD_TOTAL_BYTES / TP))
OFFLOAD_ARGS=(
--kv-transfer-config
"{\"kv_connector\":\"SimpleCPUOffloadConnector\",\"kv_role\":\"kv_both\",\"kv_connector_extra_config\":{\"kv_offload_backend\":\"disk\",\"disk_path\":\"$NVME_OFFLOAD_DIR/cache.bin\",\"disk_capacity_bytes\":$NVME_OFFLOAD_PER_RANK_BYTES,\"disk_buffer_slots\":4,\"lazy_offload\":false}}"
)
Comment on lines +103 to +109

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 WARNING: All TP ranks are pointed at the same disk_path ($NVME_OFFLOAD_DIR/cache.bin) while disk_capacity_bytes is computed per rank (8 TB / TP).

Why it matters: The per-rank capacity math implies the connector is instantiated once per TP worker (consistent with the existing vllm-simple usage in dsv4_fp4_b200_vllm_mtp.sh, which passes cpu_bytes_to_use_per_rank). If the fork's disk backend does not internally shard/suffix the path per rank, 8 workers will open and write the same 1 TB-capacity file and silently corrupt each other's cached blocks — producing wrong "cache hit" data rather than a crash. I couldn't verify the fork's SimpleCPUOffloadConnector disk backend (source unavailable in this review), and the PR's one-off validation covered the tiered OffloadingConnector path, not this NVMe-only path.

Fix: Confirm the disk backend appends a rank identifier to disk_path (and note it in a comment), or make it explicit in the config, e.g. a per-rank template/directory instead of a single cache.bin. If it does shard internally, feel free to resolve this.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed in the patched vLLM backend: SimpleCPUOffloadConnector rewrites the configured path as f"{disk_path}.rank_{device.index or 0}" (vllm/v1/simple_kv_offload/worker.py), so TP8 creates cache.bin.rank_0 through cache.bin.rank_7, each with the configured per-rank capacity. Added an inline comment beside the config to make this non-obvious behavior reviewable.

elif [ "$KV_OFFLOADING" = "dram+nvme" ]; then
require_agentic_kv_offload_backend vllm-native
: "${NVME_OFFLOAD_DIR:?NVME_OFFLOAD_DIR must be mounted by the H100 launcher}"
TOTAL_CPU_DRAM_GIB=$((TOTAL_CPU_DRAM_GB * 1000000000 / 1073741824))
PER_RANK_GIB=$(((TOTAL_CPU_DRAM_GIB - MODEL_CHECKPOINT_PAGE_CACHE_GIB) / TP - MODEL_CPU_OFFLOAD_GB - MOONCAKE_LOCAL_BUFFER_GIB))
if (( PER_RANK_GIB <= 0 )); then
echo "Error: CPU DRAM budget is too small for checkpoint cache, model, and DRAM+NVMe KV offload" >&2
exit 1
fi
CPU_OFFLOAD_TOTAL_BYTES=$((PER_RANK_GIB * TP * 1073741824))
OFFLOAD_ARGS=(
--kv-transfer-config
"{\"kv_connector\":\"OffloadingConnector\",\"kv_role\":\"kv_both\",\"kv_connector_extra_config\":{\"spec_name\":\"TieringOffloadingSpec\",\"cpu_bytes_to_use\":$CPU_OFFLOAD_TOTAL_BYTES,\"eviction_policy\":\"lru\",\"secondary_tiers\":[{\"type\":\"fs\",\"root_dir\":\"$NVME_OFFLOAD_DIR\",\"n_read_threads\":32,\"n_write_threads\":16,\"locality\":\"LOCAL\"}]}}"
)
else
echo "Error: unsupported KV_OFFLOADING='$KV_OFFLOADING'" >&2
exit 1
Expand Down
8 changes: 5 additions & 3 deletions configs/nvidia-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7341,7 +7341,7 @@ qwen3.5-fp4-b200-trt-mtp:
- { tp: 8, ep: 8, dp-attn: true, spec-decoding: "mtp", conc-list: [128, 256, 1024] }

minimaxm3-fp8-h100-vllm-agentic-mtp:
image: vllm/vllm-openai:v0.27.1
image: ttl.sh/cquil11-vllm-tier-dfb25dbef4-20260828:24h
Comment thread
cquil11 marked this conversation as resolved.
Outdated
Comment thread
cquil11 marked this conversation as resolved.
Outdated
model: MiniMaxAI/MiniMax-M3-MXFP8
model-prefix: minimaxm3
runner: cluster:h100-dgxc
Expand All @@ -7353,8 +7353,10 @@ minimaxm3-fp8-h100-vllm-agentic-mtp:
# The fast sweep places the resident HBM cliff between c5 and c6.
- dram-utilization: 0.80
search-space:
- { tp: 8, spec-decoding: mtp, kv-offloading: none, conc-list: [1, 2, 3, 4, 5] }
- { tp: 8, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: mooncake, version: "0.3.11.post1" }, conc-list: [6, 8] }
- { tp: 8, spec-decoding: mtp, kv-offloading: none, conc-list: [1, 2, 3, 4, 5, 6, 7, 8] }
- { tp: 8, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: mooncake, version: "0.3.11.post1" }, conc-list: [5, 6, 7, 8, 9, 10] }
- { tp: 8, spec-decoding: mtp, kv-offloading: nvme, kv-offload-backend: { name: vllm-simple }, conc-list: [7, 8, 9, 10, 11, 12, 13, 14] }
- { tp: 8, spec-decoding: mtp, kv-offloading: [dram, nvme], kv-offload-backend: { name: vllm-native }, conc-list: [7, 8, 9, 10, 11, 12, 13, 14] }

minimaxm3-fp8-h200-vllm-agentic-mtp:
image: vllm/vllm-openai:v0.27.1
Expand Down
12 changes: 12 additions & 0 deletions perf-changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6576,3 +6576,15 @@
- "Recipes sourced from srt-slurm (recipes/trtllm/qwen3.5-fp4/inferencex/gb300/{mtp,stp})."
- "Runner: launch_gb300-nv.sh bumped from NVIDIA/srt-slurm@v1.0.29 to v1.0.72 for the dynamo-trt+qwen3.5+fp4 path."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2730

- config-keys:
- minimaxm3-fp8-h100-vllm-agentic-mtp
scenario-type:
- agentic-coding
description:
- "Run the official H100 MiniMax-M3 AgentX sweep on the patched vLLM image that exports cached prompt tokens by physical source tier."
- "Retain the no-offload TP8 points at concurrency 1-8 and the Mooncake DRAM points at concurrency 5-10; add NVMe-only points at concurrency 7-14 using SimpleCPUOffloadConnector's disk backend with 8 TB aggregate capacity."
- "Add the same concurrency 7-14 range with declarative kv-offloading [dram, nvme], mapped by the vLLM recipe to OffloadingConnector's TieringOffloadingSpec with an LRU DRAM primary tier and node-local filesystem secondary tier."
- "Mount a job-scoped directory from the H100 node's native NVMe filesystem into the Pyxis container and remove it during allocation cleanup."
- "Collect the vLLM Prometheus endpoint through AIPerf so artifacts include vllm:prompt_tokens_cached_by_source alongside the native KV-offload tiering counters."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2775
23 changes: 21 additions & 2 deletions runners/launch_h100-dgxc-slurm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,26 @@ else
echo "ERROR: failed to resolve H100 Slurm allocation" >&2
exit 1
fi
trap 'rc=$?; scancel "$JOB_ID" 2>/dev/null || true; exit "$rc"' EXIT
NVME_HOST_DIR=""
cleanup_allocation() {
local rc=$?
trap - EXIT INT TERM
if [[ -n "$NVME_HOST_DIR" ]]; then
srun --jobid="$JOB_ID" bash -c "rm -rf -- '$NVME_HOST_DIR'" 2>/dev/null || true
fi
scancel "$JOB_ID" 2>/dev/null || true
exit "$rc"
}
Comment thread
cursor[bot] marked this conversation as resolved.
trap cleanup_allocation EXIT INT TERM
Comment on lines +301 to +307

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 BLOCKING: The NVMe KV directory leaks on every successful run.

Why it matters: On the success path, the pre-existing scancel $JOB_ID at the end of the else branch (line 345) cancels the allocation before the script exits and the EXIT trap fires. cleanup_allocation then runs srun --jobid="$JOB_ID" rm -rf … against an already-cancelled job, which fails and is swallowed by 2>/dev/null || true. So the up-to-8 TB inferencex-kv-<jobid> directory is only removed on failure paths (where set -e exits while the allocation is still alive). With 16 NVMe-backed points per sweep, successful runs will steadily fill the node-local /mnt/numa0 array.

Fix: Delete the trailing scancel $JOB_ID on line 345 and let the trap do rm→scancel in order, e.g.:

    # cleanup_allocation (EXIT trap) removes the NVMe dir, then scancels.

i.e. remove line 345 entirely — the EXIT trap already performs scancel after the rm -rf while the allocation is still alive.

Fix this →


NVME_CONTAINER_MOUNT=""
if [[ "${KV_OFFLOADING:-none}" == "nvme" || "${KV_OFFLOADING:-none}" == "dram+nvme" ]]; then
NVME_HOST_ROOT="/mnt/numa0/enroot/cache/group-$(id -g)"
NVME_HOST_DIR="$NVME_HOST_ROOT/inferencex-kv-$JOB_ID"
srun --jobid="$JOB_ID" bash -c "set -e; test -w '$NVME_HOST_ROOT'; mkdir -m 700 '$NVME_HOST_DIR'; findmnt -T '$NVME_HOST_DIR'"
NVME_CONTAINER_MOUNT=",$NVME_HOST_DIR:/kv-offload"
export NVME_OFFLOAD_DIR=/kv-offload
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancelled jobs leak NVMe cache

Medium Severity

Each NVMe job creates a host directory inferencex-kv-$JOB_ID and never removes old ones. Contents are cleared only inside the container after a graceful vLLM stop, so a cancel or timeout leaves up to 8 TB behind. The next allocation uses a new job id and can fail with no space on /mnt/numa0.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b52c639. Configure here.


# flock-serialize the enroot import so concurrent sweep jobs on the same
# shared NFS path don't race each other into 'File already exists' (race

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 On the normal success path, the explicit scancel $JOB_ID (line 345) runs before the script exits, and the subsequent EXIT trap fires cleanup_allocation (line 299-306), which tries srun --jobid="$JOB_ID" ... rm -rf on the NVMe scratch dir (line 303) against an allocation that was just cancelled. srun into an already-cancelled/completing allocation fails, and the error is swallowed by 2>/dev/null || true, so cleanup silently no-ops.

Extended reasoning...

Any nvme or dram+nvme sweep job that completes normally hits scancel $JOB_ID at line 345 first, then the EXIT trap's srun-based rm -rf against the now-cancelled job silently fails, leaking the per-job directory under /mnt/numa0/enroot/cache/group-$(id -g)/inferencex-kv-$JOB_ID on every successful run (only the error path, which skips the explicit scancel, cleans up correctly). Over a 30-job sweep with several nvme/dram+nvme rows, this accumulates orphaned directories on the shared NVMe array with no other reaper. Fix: remove the NVMe dir before calling scancel (e.g. do NVMe cleanup then scancel in one place, or drop the separate explicit scancel and rely solely on cleanup_allocation).

Verification: normal. Deterministic ordering defect newly introduced by this diff. With set -e (line 2), on a normally-completing run control reaches line 345 scancel $JOB_ID, cancelling the allocation, before the EXIT trap fires cleanup_allocation. That function's line 303 srun --jobid="$JOB_ID" bash -c "rm -rf -- '$NVME_HOST_DIR'" then launches a new step into the just-cancelled job; Slurm…

Expand All @@ -317,7 +336,7 @@ else

srun --jobid=$JOB_ID \
--container-image=$SQUASH_FILE \
--container-mounts=$GITHUB_WORKSPACE:/workspace/,$HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE,$AIPERF_MMAP_CACHE_HOST_PATH:/aiperf_mmap_cache \
--container-mounts=$GITHUB_WORKSPACE:/workspace/,$HF_HUB_CACHE_MOUNT:$HF_HUB_CACHE,$AIPERF_MMAP_CACHE_HOST_PATH:/aiperf_mmap_cache$NVME_CONTAINER_MOUNT \
--no-container-mount-home \
--container-workdir=/workspace/ \
--no-container-entrypoint --export=ALL,PORT=8888,AIPERF_DATASET_MMAP_CACHE_DIR=/aiperf_mmap_cache \
Expand Down
22 changes: 15 additions & 7 deletions utils/matrix_logic/generate_sweep_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ def agentic_dram_offload_gb(
budgeted separately if it ever gains its own pool).
"""
kv_offloading = benchmark.get(Fields.KV_OFFLOADING.value, "none")
if kv_offloading != "dram":
if kv_offloading != "dram" and kv_offloading != ["dram", "nvme"]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📊 Line Count Report

  • Total Lines: 1650
  • Base Lines: 1642
  • Change: 📈 +8 lines

return 0

available_mib = min(
Expand Down Expand Up @@ -356,13 +356,21 @@ def agentic_dram_offload_gb(


def agentic_kv_offload_suffix(
kv_offloading: str,
kv_offloading: str | list[str],
kv_offload_backend: dict | None,
) -> str:
"""Return a compact exp-name suffix for agentic KV offload settings."""
if kv_offloading == "none":
return "kvnone"
return f"kv{kv_offloading}-{kv_offload_backend['name']}"
mode = "+".join(kv_offloading) if isinstance(kv_offloading, list) else kv_offloading
return f"kv{mode}-{kv_offload_backend['name']}"


def agentic_kv_offload_runtime_value(kv_offloading: str | list[str]) -> str:
"""Convert declarative tier lists into the workflow's string input."""
if isinstance(kv_offloading, list):
return "+".join(kv_offloading)
return kv_offloading


def multinode_agentic_exp_name(
Expand Down Expand Up @@ -1029,7 +1037,7 @@ def generate_full_sweep(args, all_config_data, runner_data):
Fields.PREFILL.value: prefill,
Fields.DECODE.value: decode,
Fields.CONC.value: conc_batch,
Fields.KV_OFFLOADING.value: kv_offloading,
Fields.KV_OFFLOADING.value: agentic_kv_offload_runtime_value(kv_offloading),
Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb,
Fields.DURATION.value: duration,
Fields.EXP_NAME.value: multinode_agentic_exp_name(
Expand Down Expand Up @@ -1066,7 +1074,7 @@ def generate_full_sweep(args, all_config_data, runner_data):
Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False,
Fields.SPEC_DECODING.value: spec_decoding,
Fields.CONC.value: conc,
Fields.KV_OFFLOADING.value: kv_offloading,
Fields.KV_OFFLOADING.value: agentic_kv_offload_runtime_value(kv_offloading),
Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb,
Fields.DURATION.value: duration,
Fields.EXP_NAME.value: (
Expand Down Expand Up @@ -1328,7 +1336,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None):
Fields.PREFILL.value: prefill,
Fields.DECODE.value: decode,
Fields.CONC.value: conc_batch,
Fields.KV_OFFLOADING.value: kv_offloading,
Fields.KV_OFFLOADING.value: agentic_kv_offload_runtime_value(kv_offloading),
Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb,
Fields.DURATION.value: duration,
Fields.EXP_NAME.value: multinode_agentic_exp_name(
Expand Down Expand Up @@ -1364,7 +1372,7 @@ def generate_test_config_sweep(args, all_config_data, runner_data=None):
Fields.DP_ATTN.value: dp_attn if dp_attn is not None else False,
Fields.SPEC_DECODING.value: spec_decoding,
Fields.CONC.value: conc,
Fields.KV_OFFLOADING.value: kv_offloading,
Fields.KV_OFFLOADING.value: agentic_kv_offload_runtime_value(kv_offloading),
Fields.TOTAL_CPU_DRAM_GB.value: total_cpu_dram_gb,
Fields.DURATION.value: duration,
Fields.EXP_NAME.value: (
Expand Down
50 changes: 50 additions & 0 deletions utils/matrix_logic/test_generate_sweep_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2385,6 +2385,56 @@ def test_agentic_node_dram_uses_explicit_gpu_count(self, sample_runner_config):
}
assert all(entry["duration"] == 3600 for entry in result)

def test_multi_tier_agentic_uses_dram_budget_and_distinct_name(
self, sample_runner_config
):
config = {
"dsv4-b300-agentic": {
"image": "vllm/vllm-openai:v0.23.0",
"model": "deepseek-ai/DeepSeek-V4-Pro",
"model-prefix": "dsv4",
"precision": "fp4",
"framework": "vllm",
"runner": "cluster:b300-nv",
"multinode": False,
"scenarios": {
"agentic-coding": [{
"dram-utilization": 0.80,
"search-space": [
{
"tp": 8,
"kv-offloading": "nvme",
"kv-offload-backend": {"name": "vllm-simple"},
"conc-list": [7],
},
{
"tp": 8,
"kv-offloading": ["dram", "nvme"],
"kv-offload-backend": {"name": "vllm-native"},
"conc-list": [7],
},
],
}],
},
},
}
args = argparse.Namespace(
config_keys=["dsv4-b300-agentic"],
seq_lens=None,
conc=None,
scenario_type=["agentic-coding"],
runner_node_filter=None,
)

result = generate_test_config_sweep(args, config, sample_runner_config)

assert [entry["kv-offloading"] for entry in result] == ["nvme", "dram+nvme"]
assert [entry["total-cpu-dram-gb"] for entry in result] == [0, 2399]
assert [entry["exp-name"] for entry in result] == [
"dsv4_tp8_conc7_kvnvme-vllm-simple",
"dsv4_tp8_conc7_kvdram+nvme-vllm-native",
]

def test_agentic_node_dram_rejects_tp_above_runner_gpus(self, sample_runner_config):
config = {
"dsv4-b300-agentic": {
Expand Down
25 changes: 25 additions & 0 deletions utils/matrix_logic/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,31 @@ def test_dram_kv_offload_requires_dram_utilization(self):
}],
})

def test_multi_tier_kv_offload_requires_dram_utilization(self):
with pytest.raises(Exception, match="dram-utilization"):
AgenticCodingConfig(**{
"search-space": [{
"tp": 8,
"kv-offloading": ["dram", "nvme"],
"kv-offload-backend": {"name": "vllm-native"},
"conc-list": [7, 8],
}],
})

def test_multi_tier_kv_offload_accepts_dram_capacity_config(self):
config = AgenticCodingConfig(**{
"dram-utilization": 0.99,
"search-space": [{
"tp": 8,
"kv-offloading": ["dram", "nvme"],
"kv-offload-backend": {"name": "vllm-native"},
"conc-list": [7, 8],
}],
})

assert config.search_space[0].kv_offloading == ["dram", "nvme"]
assert config.dram_utilization == 0.99

def test_agentic_search_space_rejects_total_cpu_dram_gb(self):
with pytest.raises(Exception, match="total-cpu-dram-gb"):
AgenticCodingSearchSpaceEntry(**{
Expand Down
19 changes: 15 additions & 4 deletions utils/matrix_logic/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@

CLUSTER_LABEL_PREFIX = "cluster:"
DEFAULT_AGENTIC_DURATION_SECONDS = 3600
KVOffloadingTier = Literal["dram", "nvme"]
KVOffloadingConfig = Union[
Literal["none", "dram", "nvme"],
List[KVOffloadingTier],
]

"""
The below class defines the field names expected to be present in the JSON entries
Expand Down Expand Up @@ -318,7 +323,7 @@ class SingleNodeAgenticMatrixEntry(BaseModel):
default="none", alias=Fields.SPEC_DECODING.value
)
conc: int
kv_offloading: Literal["none", "dram"] = Field(
kv_offloading: Literal["none", "dram", "nvme", "dram+nvme"] = Field(
alias=Fields.KV_OFFLOADING.value
)
kv_offload_backend: Optional[KVOffloadBackendMetadata] = Field(
Expand Down Expand Up @@ -513,7 +518,13 @@ def _validate_kv_offload_fields(self):
f"{Fields.KV_OFFLOADING.value}"
)
return self
if self.kv_offloading == "none":
if isinstance(self.kv_offloading, list):
if self.kv_offloading != ["dram", "nvme"]:
raise ValueError(
f"The only supported multi-tier {Fields.KV_OFFLOADING.value} "
"configuration is ['dram', 'nvme']"
)
elif self.kv_offloading == "none":
if backend is not None:
raise ValueError(
f"{Fields.KV_OFFLOAD_BACKEND.value} can only be set when "
Expand Down Expand Up @@ -644,7 +655,7 @@ class AgenticCodingSearchSpaceEntry(BaseModel):
decode: Optional[WorkerConfig] = None
num_nodes: Optional[int] = Field(
default=None, alias=Fields.NUM_NODES.value, gt=0, strict=True)
kv_offloading: Optional[Literal["none", "dram"]] = Field(
kv_offloading: Optional[KVOffloadingConfig] = Field(
default=None, alias=Fields.KV_OFFLOADING.value
)
kv_offload_backend: Optional[KVOffloadBackendMetadata] = Field(
Expand Down Expand Up @@ -726,7 +737,7 @@ class AgenticCodingConfig(BaseModel):
@model_validator(mode='after')
def validate_dram_offload_capacity(self):
for entry in self.search_space:
if entry.kv_offloading != "dram":
if entry.kv_offloading != "dram" and entry.kv_offloading != ["dram", "nvme"]:
continue
if self.dram_utilization is None:
raise ValueError(
Expand Down
Loading