Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cd71b9e
kimik3-fp4-b300-vllm-agentic-dspark: add LMCache DRAM KV-offload arm
sammshen Aug 13, 2026
57a9b88
perf-changelog: point the LMCache B300 entry at #2593
sammshen Aug 13, 2026
dcf60b5
kimik3-fp4-b300-vllm-agentic-dspark-lmcache: sweep conc 4/8/10/16
sammshen Aug 13, 2026
b529bef
kimik3 b300 lmcache: install the published cu129 wheel
sammshen Aug 13, 2026
07e114d
kimik3 b300 lmcache: chunk size 3072 to match the 1536 attention block
sammshen Aug 13, 2026
eebc552
perf-changelog: point the LMCache B300 entry at #2597
sammshen Aug 14, 2026
4adc0bf
Merge remote-tracking branch 'upstream/main' into kimik3-b300-lmcache
sammshen Aug 14, 2026
2eddc89
kimik3 b300 lmcache: install the CUDA-13 wheel and fail on c_ops fall…
sammshen Aug 14, 2026
928caeb
Merge remote-tracking branch 'upstream/main' into kimik3-b300-lmcache
sammshen Aug 14, 2026
25e305f
kimik3 b300 lmcache: force-reinstall lmcache; the pin was a silent no-op
sammshen Aug 14, 2026
6c34017
Merge remote-tracking branch 'upstream/main' into kimik3-b300-lmcache
sammshen Aug 14, 2026
42a9f4a
kimik3 b300 lmcache: surface the real c_ops dlopen error
sammshen Aug 14, 2026
cb45449
kimik3 b300 lmcache: put torch's lib dir on LD_LIBRARY_PATH for c_ops
sammshen Aug 14, 2026
51d2951
kimik3 b300 lmcache: install plain from PyPI; probe c_ops import order
sammshen Aug 14, 2026
351466a
Merge remote-tracking branch 'upstream/main' into kimik3-b300-lmcache
Aug 14, 2026
f441071
kimik3 b300 lmcache: launch the MP server with torch preloaded
sammshen Aug 14, 2026
273ab13
kimik3 b300 lmcache: instrument c_ops to capture the swallowed error
sammshen Aug 14, 2026
7a31be3
kimik3 b300 lmcache: LD_PRELOAD torch libs so c_ops resolves c10 symbols
sammshen Aug 14, 2026
9fdd6de
kimik3 b300 lmcache: locate which torch lib defines materialize_cow_s…
sammshen Aug 14, 2026
0e29d17
kimik3 b300 lmcache: check the image's own lmcache before overwriting it
sammshen Aug 14, 2026
32f721d
trigger sweep: probe image-bundled lmcache c_ops
sammshen Aug 14, 2026
3044c26
kimik3 b300 lmcache: characterise the torch/c10 COW ABI gap
sammshen Aug 14, 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
291 changes: 290 additions & 1 deletion benchmarks/single_node/agentic/kimik3_fp4_b300_vllm_mtp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,20 @@ export AIPERF_HTTP_TCP_USER_TIMEOUT=900000
SERVER_LOG="$RESULT_DIR/server.log"
mkdir -p "$RESULT_DIR"

# The LMCache arm starts a second long-lived process that must not outlive this
# job (its L1 holds the whole host-DRAM budget). The vLLM server's lifecycle is
# left exactly as it was -- the job wrapper still owns it.
LMCACHE_PID=""

cleanup_lmcache_server() {
local exit_code=$?
trap - EXIT
set +e
stop_background_process_tree "$LMCACHE_PID" "LMCache server"
exit "$exit_code"
}
trap cleanup_lmcache_server EXIT
Comment on lines +123 to +135

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.

🟡 cleanup_lmcache_server (kimik3_fp4_b300_vllm_mtp.sh:134-135) is only trapped on EXIT, unlike both sibling lmcache scripts (minimaxm3_fp4_mi355x_mtp.sh:75-77, dsv4_fp4_mi355x_vllm_mtp.sh:242-244), which additionally trap INT and TERM via trap 'exit 130' INT / trap 'exit 143' TERM. Without that, an untrapped SIGINT/SIGTERM delivered to this script's own PID skips the EXIT trap entirely, so an external cancellation could leave the LMCache MP server running and holding the whole host-DRAM budget on the node. Fix by adding the same two trap lines after trap cleanup_lmcache_server EXIT.

Extended reasoning...

The bug: kimik3_fp4_b300_vllm_mtp.sh:135 registers trap cleanup_lmcache_server EXIT and nothing else. Both existing lmcache-using siblings — minimaxm3_fp4_mi355x_mtp.sh:75-77 and dsv4_fp4_mi355x_vllm_mtp.sh:242-244 — register the identical cleanup on EXIT and additionally do trap 'exit 130' INT and trap 'exit 143' TERM. This new arm drops those last two lines.

Why it matters: in bash, a signal (SIGINT/SIGTERM) delivered directly to a script's own PID while the script is blocked on a foreground child (here, wait_for_server_ready/the vLLM server) does not reliably invoke a plain trap ... EXIT handler — the shell can terminate from the signal without ever reaching the EXIT trap. The trap 'exit N' INT TERM idiom used by both siblings exists specifically to convert the signal into a normal exit, which does run the EXIT trap and its cleanup. I reproduced this directly: a minimal bash script with an EXIT-only trap that backgrounds a child and blocks on wait , when sent SIGINT or SIGTERM targeting its own PID, left the script running (or exited without invoking cleanup) and orphaned the backgrounded child — the fix (adding the INT/TERM traps) reliably ran the cleanup in the same harness.

Why nothing else catches this: the job wrapper here only owns the vLLM server's lifecycle (per the script's own comment), not LMCache's — that's the entire reason cleanup_lmcache_server was added as a dedicated trap in this PR. There's no other supervisor watching for an orphaned LMCache process once the script itself exits abnormally via signal.

Impact: the script's own comment states the LMCache server "must not outlive this job (its L1 holds the whole host-DRAM budget)" — at this config's dram-utilization: 0.63/TP8, that's ~1,889 GB of host DRAM. An external cancellation or timeout that signals the script's PID directly (the same class of external-termination path this PR's own SKILL.md update calls out, e.g. jobs with null step conclusions from external termination) can leave that MP server running, stranding the DRAM and blocking subsequent jobs on the node.

Proof walkthrough: (1) job is cancelled/times out; the runner sends SIGTERM/SIGINT to the script's PID while it's blocked in wait_for_server_ready on the vLLM child. (2) Only an EXIT trap is registered, so the shell can exit from the signal without invoking cleanup_lmcache_server. (3) stop_background_process_tree "" ... never runs. (4) The LMCache MP server (started earlier in the script, listening on 127.0.0.1:6555/8090) keeps running, holding its --l1-size-gb $TOTAL_CPU_DRAM_GB allocation. (5) The next job scheduled on that node now competes for or is blocked by that stranded DRAM allocation.

Fix: add the same two lines the siblings already use, immediately after line 135:

trap cleanup_lmcache_server EXIT
trap 'exit 130' INT
trap 'exit 143' TERM

This is a one-line divergence from an established two-script convention with a real (if narrow — normal completion and process-group signal delivery are unaffected) operational cost, so it's a nit worth fixing to match the siblings rather than a blocker.


# ---- KV offloading ----------------------------------------------------------
# The generated TOTAL_CPU_DRAM_GB budget is the aggregate host-DRAM pool for the
Comment on lines +123 to 138

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.

🟡 sweep:cleanup_(lmcache_server|agentic_services)()
This file's cleanup_lmcache_server() (kimik3_fp4_b300_vllm_mtp.sh:126-138) hand-rolls the capture-$?/trap-EXIT/set-+e/stop_background_process_tree/exit-$exit_code idiom to reap one auxiliary background PID; dsv4_fp4_mi355x_vllm_mtp.sh's cleanup_lmcache_server/cleanup_agentic_services do the same for one-to-three PIDs. A benchmark_lib.sh helper like register_cleanup_pid "$PID" "label" that accumulates tracked PIDs and installs the trap once would collapse each call site to a single line.

Extended reasoning...

Every agentic script that spawns an auxiliary long-lived process (an LMCache MP-server, a router, a Mooncake master, etc.) alongside the main vLLM/SGLang server needs to guarantee that process is killed when the job exits, with the original exit code preserved so CI still reports pass/fail correctly. The pattern used to do this — local exit_code=$?; trap - EXIT; set +e; stop_background_process_tree "$PID" "label"; exit "$exit_code", installed via trap cleanup_fn EXIT — is hand-authored independently in this PR's new cleanup_lmcache_server() (kimik3_fp4_b300_vllm_mtp.sh:126-138), in dsv4_fp4_mi355x_vllm_mtp.sh's cleanup_lmcache_server/cleanup_agentic_services, and in minimaxm3_fp4_mi355x_mtp.sh's cleanup_agentic_services (which iterates an LMCACHE_PIDS array). Several verifiers additionally found the same idiom repeated across roughly a dozen sibling scripts (minimaxm3's B200/B300 MTP variants, both qwen3.5 MTP variants, kimik3_fp4_mi355x_mtp.sh, and the deprecated kimik2.5_fp4_b200.sh).\n\nThe underlying primitive, stop_background_process_tree, is already centralized in benchmark_lib.sh:284 and every one of these scripts correctly calls into it — so this isn't a case of unshared kill logic. What's duplicated is the thin wrapper around it: capturing the pre-trap exit code, disarming the trap so it doesn't re-fire, relaxing set -e so the cleanup itself can't mask the original failure, and re-exiting with the preserved code. That's exactly the kind of small, easy-to-get-subtly-wrong boilerplate (e.g. forgetting set +e, or exiting 0 instead of the captured code) that benchmark_lib.sh exists to hold once. A helper such as register_cleanup_pid "$PID" "label" that appends to a global PID/label list and lazily installs a single shared EXIT trap would let each call site shrink to one line, while still accommodating the single-PID (this PR, dsv4's LMCache arm), multi-PID (dsv4's router+server+Mooncake), and array (minimaxm3's LMCACHE_PIDS) shapes seen today.\n\nOne verifier refuted this on scope grounds: the idiom recurs in roughly a dozen sibling scripts that this PR does not touch, so consolidating it here would make this script inconsistent with its un-refactored siblings, and the maintainers' apparent choice not to abstract it despite ~12 repetitions is itself signal that the per-script copy is the accepted convention. That's a reasonable caution against silently introducing a bespoke abstraction in a one-arm PR, but it doesn't change the underlying fact being reported: the duplication is real, growing (this PR adds yet another copy), and a shared helper is a plausible, low-risk win a maintainer could pick up in one pass across benchmark_lib.sh plus the handful of call sites, rather than being blocked on this PR alone. It's included as a quality/reuse observation, not a request to refactor unrelated scripts as part of this change.\n\nNothing is functionally wrong with the block as written — the trap correctly disarms itself, preserves the exit code, and lets set +e neutralize this script's set -eo pipefail for the duration of the cleanup call. This finding does not block merge; it's a maintainability nit best addressed as a standalone benchmark_lib.sh change touched by whoever next has occasion to edit one of these cleanup blocks.

# node; SimpleCPUOffloadConnector is sized per rank. At dram-utilization 0.63 on
Expand All @@ -142,8 +156,283 @@ case "${KV_OFFLOAD_BACKEND:-}" in
"{\"kv_connector\":\"SimpleCPUOffloadConnector\",\"kv_role\":\"kv_both\",\"kv_connector_extra_config\":{\"cpu_bytes_to_use_per_rank\":${CPU_BYTES_PER_RANK},\"lazy_offload\":false}}"
)
;;
lmcache)
require_agentic_kv_offload_backend lmcache

# Plain PyPI. The GitHub release `expanded_assets` URLs were never
# doing anything here -- pip resolved the identical PyPI wheel
# (lmcache-0.5.4rc2-cp312-cp312-manylinux_2_27_x86_64...whl, 15.6 MB)
# in every run, whether --find-links pointed at the -cu129 assets or
# the default ones.
#
# --force-reinstall IS required: this image ships lmcache 0.5.3, so a
# plain install of 0.5.4rc2 upgrades it, but without the flag an
# already-matching version would be left in place silently.
#
# Why any of this matters: LMCache does not fail when its compiled
# `lmcache.c_ops` extension cannot be loaded. It logs "compiled
# extension not found; CudaDeviceOps stays on the torch baseline" and
# silently falls back to lmcache/v1/platform/torch_ops.py, which is
# broken for this stack's hybrid multi-KV-group KDA/MLA layout: 77-98%
# of stores die with cudaErrorInvalidValue, so the offload tier stays
# empty and the arm measures a cache that never stored anything.
# The guard after server start is what makes that loud.
# --force-reinstall is REQUIRED, not cosmetic: this image already ships
# lmcache 0.5.4rc2, so a plain `pip install lmcache==0.5.4rc2` finds the
# requirement already satisfied and installs nothing -- the --find-links
# URL is never consulted. Two earlier sweeps proved it: one pinned to
# the -cu129 assets and one to the default assets both produced the
# identical build, `LMCache v0.5.4rc2 (gf82f6fd3)`, in ~3 seconds. We
# were always running the image's copy, whose c_ops does not load here.
# Before overwriting it: does the image's OWN lmcache work? The
# 0.5.4rc2 wheel's c_ops needs c10::impl::cow::materialize_cow_storage,
# which nm shows NO torch library in this image defines -- a real ABI
# mismatch, not an ordering problem. The image ships 0.5.3, presumably
# built against this torch, so establish whether it loads before we
# replace it.
echo "=== image's pre-existing lmcache ==="
python3 -m pip show lmcache 2>/dev/null | grep -E "^Version:" || echo " (not installed)"
_IMG_SO=$(python3 -c "import lmcache, glob, os; print((glob.glob(os.path.join(os.path.dirname(lmcache.__file__), 'c_ops*.so')) or [''])[0])" 2>/dev/null || true)
if [ -n "$_IMG_SO" ]; then
echo " c_ops: $_IMG_SO"
echo " needs materialize_cow_storage: $(nm -D --undefined-only "$_IMG_SO" 2>/dev/null | grep -c materialize_cow_storage || true)"
python3 -c "import lmcache.c_ops; print(' IMAGE c_ops import: OK')" 2>&1 | tail -2 || echo " IMAGE c_ops import: FAILED"
fi

LMCACHE_VERSION="0.5.4rc2"
agentic_pip_install --no-cache-dir --force-reinstall --no-deps \
"lmcache==${LMCACHE_VERSION}"
# Record what actually landed. Silent installs are how the no-op above
# went unnoticed for two full sweeps; do not add --quiet back.
python3 -m pip show lmcache 2>/dev/null | grep -E "^(Version|Location):"
python3 -c "import lmcache, glob, os; print('lmcache:', lmcache.__file__); print('c_ops so:', glob.glob(os.path.join(os.path.dirname(lmcache.__file__), 'c_ops*')))"
ls -1 /usr/local/cuda*/lib64/libcudart.so* /usr/lib/x86_64-linux-gnu/libcudart.so* 2>/dev/null || true
# device_ops.ensure_native() swallows this in `except ImportError`, so
# surface the real dlopen error ourselves -- without it the failure is
# indistinguishable from a missing file.
python3 - <<'PYEOF' || true
# Definitive c_ops instrumentation.
#
# ensure_native() swallows the real exception in `except ImportError` and
# latches _native_bound=True on the first attempt, so neither the error nor the
# caller is ever visible. Wrap __import__ to capture the exception, and hook the
# logger to capture the stack at the moment the warning is emitted -- together
# these name both WHAT fails and WHO triggered it.
import builtins
import logging
import sys
import traceback

_real_import = builtins.__import__
_failures = []


def _tracing_import(name, globals=None, locals=None, fromlist=(), level=0):
try:
return _real_import(name, globals, locals, fromlist, level)
except BaseException as exc: # noqa: BLE001 - diagnostic
if "c_ops" in name:
_failures.append((name, exc))
print(f"[probe] IMPORT FAILED: {name}: {type(exc).__name__}: {exc}", flush=True)
traceback.print_exc()
raise


class _WarnHook(logging.Handler):
def emit(self, record):
try:
msg = record.getMessage()
except Exception: # noqa: BLE001
return
if "c_ops compiled extension not found" in msg:
print("[probe] ensure_native() gave up -- caller stack:", flush=True)
traceback.print_stack()


builtins.__import__ = _tracing_import
logging.getLogger().addHandler(_WarnHook())
logging.getLogger().setLevel(logging.DEBUG)

print("[probe] torch loaded before lmcache?", "torch" in sys.modules, flush=True)

# Import exactly what the server imports, in the server's order.
import lmcache.integration.vllm.lmcache_mp_connector # noqa: E402,F401

print("[probe] torch in sys.modules now:", "torch" in sys.modules, flush=True)
print("[probe] c_ops in sys.modules:", "lmcache.c_ops" in sys.modules, flush=True)
print(f"[probe] captured c_ops import failures: {len(_failures)}", flush=True)
for name, exc in _failures:
print(f"[probe] {name}: {type(exc).__name__}: {exc}", flush=True)

# Now show whether the singleton is latched off despite the extension being loadable.
try:
from lmcache.v1.platform.cuda.device_ops import CudaDeviceOps

ops = CudaDeviceOps()
print("[probe] CudaDeviceOps._native_bound =", getattr(ops, "_native_bound", "?"), flush=True)
except BaseException as exc: # noqa: BLE001
print("[probe] could not inspect CudaDeviceOps:", type(exc).__name__, exc, flush=True)

# And whether a direct import works at this point.
try:
import lmcache.c_ops # noqa: F401

print("[probe] direct import lmcache.c_ops AFTER: OK", flush=True)
except BaseException as exc: # noqa: BLE001
print("[probe] direct import lmcache.c_ops AFTER FAILED:", type(exc).__name__, exc, flush=True)
PYEOF
# c_ops.so links libtorch/libc10 but they are not on the default
# loader path -- `ldd` reports libc10.so, libtorch.so, libtorch_cpu.so,
# libtorch_python.so, libc10_cuda.so and libtorch_cuda.so as "not
# found" (libcudart resolves fine). Normally that is harmless because
# `import torch` loads them RTLD_GLOBAL first, but
# CudaDeviceOps.ensure_native() sets `self._native_bound = True` BEFORE
# its `import lmcache.c_ops`, so one early failure -- before torch is
# in the process -- permanently disables native ops and silently pins
# the whole server to the broken torch fallback. Putting torch's lib
# dir on LD_LIBRARY_PATH makes the extension loadable regardless of
# import order.
TORCH_LIB_DIR=$(python3 -c "import os, torch; print(os.path.join(os.path.dirname(torch.__file__), 'lib'))")
export LD_LIBRARY_PATH="${TORCH_LIB_DIR}${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH"

# ldd names the missing/unresolved shared object directly.
LMCACHE_SO=$(python3 -c "import lmcache, glob, os; print((glob.glob(os.path.join(os.path.dirname(lmcache.__file__), 'c_ops*.so')) or [''])[0])")
if [ -n "$LMCACHE_SO" ]; then
echo "ldd $LMCACHE_SO"
ldd "$LMCACHE_SO" 2>&1 | grep -E "not found|libcudart|libtorch|libc10" || true
fi
# Which torch library actually EXPORTS the symbol c_ops needs?
# LD_PRELOAD of libc10/libtorch_cpu/libtorch did not resolve it, so
# locate the definition rather than guessing which .so to preload.
# Which torch is this, and what does its c10 actually export under
# c10::impl::cow? The wheel embeds no torch version, so the mismatch
# has to be characterised from the symbols. c_ops needs BOTH
# c10::impl::cow::is_cow_data_ptr and ::materialize_cow_storage.
python3 -c "import torch; print(' torch:', torch.__version__, '| built with CUDA', torch.version.cuda)"
echo " --- all c10::impl::cow symbols exported by libc10.so ---"
nm -D --defined-only "$TORCH_LIB_DIR/libc10.so" 2>/dev/null | grep -oE "_ZN3c104impl3cow[A-Za-z0-9_]*" | sort -u | head -20 || true
echo " --- (count) ---"
nm -D --defined-only "$TORCH_LIB_DIR/libc10.so" 2>/dev/null | grep -c "3cow" || true
echo " --- what c_ops requires ---"
nm -D --undefined-only "$LMCACHE_SO" 2>/dev/null | grep -oE "_ZN3c104impl3cow[A-Za-z0-9_]*" | sort -u || true

echo "=== searching torch libs for materialize_cow_storage ==="
for _so in "$TORCH_LIB_DIR"/libc10.so "$TORCH_LIB_DIR"/libc10_cuda.so \
"$TORCH_LIB_DIR"/libtorch.so "$TORCH_LIB_DIR"/libtorch_cpu.so \
"$TORCH_LIB_DIR"/libtorch_cuda.so "$TORCH_LIB_DIR"/libtorch_python.so; do
[ -f "$_so" ] || continue
_n=$(nm -D --defined-only "$_so" 2>/dev/null | grep -c "materialize_cow_storage" || true)
_u=$(nm -D --undefined-only "$_so" 2>/dev/null | grep -c "materialize_cow_storage" || true)
echo " $(basename "$_so"): defined=$_n undefined=$_u"
done
echo " (c_ops needs it) $(nm -D --undefined-only "$LMCACHE_SO" 2>/dev/null | grep -c materialize_cow_storage || true)"

python3 -c \
"import cupy; import lmcache.integration.vllm.lmcache_mp_connector; import opentelemetry.exporter.prometheus" \
>/dev/null

# One MP server for the node, per the Kimi-K3 recipe
# (docs.lmcache.ai/recipes/kimi_k3.html), but NOT that recipe's
# --chunk-size 768: the connector requires the chunk to be a multiple
# of every engine KV group's tokens_per_block, and this stack's blocks
# are far larger than 768. On this exact image and script, vLLM pins
# the attention group to 1536 (run 31404943911, interface.py:911,
# "Setting attention block size to 1536 tokens to ensure that
# attention page size is >= mamba page size") and pads the KDA/mamba
# page to match (interface.py:935). That alignment is generic vLLM
# code, not platform-specific, which is why the MI355X sister arm
# lands on the same 1536 plus a 3072-token KDA state group and also
# needs 3072. 768 is smaller than the attention block, so it is not a
# multiple of it and fails at connector init. The multi-group layout
# additionally requires one object group per sliding-window size:
# --separate-object-groups.
LMCACHE_PORT=6555
LMCACHE_HTTP_PORT=8090
LMCACHE_LOG="$RESULT_DIR/lmcache_server.log"

# Consume the generated aggregate budget verbatim, per
# benchmarks/single_node/agentic/README.md. --shm-name "" keeps L1 in
# ordinary process memory so the budget is not silently capped by the
# size of the node's /dev/shm mount.
LMCACHE_L1_SIZE_GB="$TOTAL_CPU_DRAM_GB"

# c_ops.so resolves c10/torch symbols only once libtorch is loaded
# RTLD_GLOBAL. Captured directly:
#
# ImportError: .../lmcache/c_ops.cpython-312-x86_64-linux-gnu.so:
# undefined symbol: _ZN3c104impl3cow23materialize_cow_storageERNS_11StorageImplE
# ( c10::impl::cow::materialize_cow_storage(c10::StorageImpl&) )
#
# It is NOT an ABI mismatch -- the same import succeeds moments later,
# once torch is in the process, so the symbol does exist in this
# image's torch. But CudaDeviceOps.ensure_native() latches
# _native_bound = True on its FIRST attempt and swallows the error, so
# one early miss pins the process to the torch fallback for good, and
# that fallback is broken for this stack's multi-KV-group KDA/MLA
# layout (77-98% of stores fail).
#
# Preloading torch in the parent is not enough: the server forks CPU
# and GPU workers that import lmcache fresh. LD_PRELOAD applies to the
# whole process tree, so the symbols are resolvable everywhere. Scoped
# to this command via `env` rather than exported, to leave the vLLM
# server's environment untouched.
LMCACHE_LD_PRELOAD="${TORCH_LIB_DIR}/libc10.so:${TORCH_LIB_DIR}/libtorch_cpu.so:${TORCH_LIB_DIR}/libtorch.so"
if [ -f "${TORCH_LIB_DIR}/libc10_cuda.so" ]; then
LMCACHE_LD_PRELOAD="${LMCACHE_LD_PRELOAD}:${TORCH_LIB_DIR}/libc10_cuda.so:${TORCH_LIB_DIR}/libtorch_cuda.so"
fi
echo "LMCACHE_LD_PRELOAD=$LMCACHE_LD_PRELOAD"
LMCACHE_CMD=(
env "LD_PRELOAD=$LMCACHE_LD_PRELOAD"
lmcache server
--host 127.0.0.1
--port "$LMCACHE_PORT"
--http-host 127.0.0.1
--http-port "$LMCACHE_HTTP_PORT"
--l1-size-gb "$LMCACHE_L1_SIZE_GB"
--l1-init-size-gb 10
--chunk-size 3072
--separate-object-groups
--enable-extra-logging
--extra-logging-interval 30
--max-cpu-workers 8
--max-gpu-workers 1
--eviction-policy LRU
--supported-transfer-mode lmcache_driven
--shm-name ""
)
append_command "$RESULT_DIR/lmcache_command.txt" "${LMCACHE_CMD[@]}"
"${LMCACHE_CMD[@]}" > "$LMCACHE_LOG" 2>&1 &
LMCACHE_PID=$!
wait_for_ready \
--endpoint "http://127.0.0.1:${LMCACHE_HTTP_PORT}/healthcheck" \
--log "$LMCACHE_LOG" \
--pid "$LMCACHE_PID" \
--sleep-interval 1 \
--timeout 600

# Second guard, on the server process itself: the import check above
# only proves c_ops loads in this shell's python3. Abort rather than
# benchmark a silently-degraded offload tier (see the install note).
if grep -q "c_ops compiled extension not found" "$LMCACHE_LOG"; then
echo "Error: LMCache fell back to the torch baseline (c_ops did not load)." >&2
echo " The installed lmcache's compiled extension is unusable here." >&2
echo " Check the pip show / c_ops / libcudart lines logged above:" >&2
echo " either the install did not take, or the build's CUDA ABI" >&2
echo " does not match this image." >&2
grep -m1 "c_ops compiled extension not found" "$LMCACHE_LOG" >&2
exit 1
fi

# 100k-330k-token agentic prefixes make single retrieves large; use the
Comment on lines +407 to +427

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.

🟡 The lmcache arm calls wait_for_ready (benchmark_lib.sh) as a plain foreground statement, and that function's first line is set +x with no restore — so for this arm only, xtrace is silently disabled ~90 lines before the script's own explicit set +x at line 321. That swallows trace output for the DSpark SPEC_CONFIG build, MAX_NUM_SEQS, the CUDA_GRAPH_CAPTURE_SIZES loop, and COMPILATION_CONFIG assembly that the none/vllm-simple arms still trace. A one-line set -x right after the wait_for_ready call in the lmcache case (kimik3_fp4_b300_vllm_mtp.sh:241) restores parity.

Extended reasoning...

wait_for_ready() in benchmarks/benchmark_lib.sh (line 349) begins with set +x and never re-enables xtrace before it returns. In this script it is invoked as a plain foreground statement inside the new lmcache) case arm (kimik3_fp4_b300_vllm_mtp.sh:234-239), not in a subshell or pipeline, so the set +x leaks straight into the caller's shell and stays off for the rest of the script.

The script itself only toggles xtrace twice: set -x at line 3, and { set +x; } 2>/dev/null at line 321, immediately before the VLLM_CMD array is assembled. For the none/vllm-simple arms — which never call wait_for_ready inside the case — xtrace stays ON from line 3 all the way through the DSpark SPEC_CONFIG string construction, MAX_NUM_SEQS, the CUDA_GRAPH_CAPTURE_SIZES enumeration loop, and the COMPILATION_CONFIG assembly, and is only turned off at line 321 as intended. For the lmcache arm, the exact same block of code runs with xtrace already OFF, because wait_for_ready cut it roughly 90 lines earlier at line 234-239. This is a real, PR-introduced asymmetry: wait_for_ready for the vLLM server itself (wait_for_server_ready, called after line 321) never surfaces this because it runs after the explicit set +x anyway, so the discrepancy is unique to the new lmcache code path.

Concretely, this means the job log for an lmcache-arm run silently loses the trace lines that would show how SPEC_CONFIG, MAX_NUM_SEQS, CUDA_GRAPH_CAPTURE_SIZES, and COMPILATION_CONFIG were built — variables specific to this PR's own DSpark/MTP logic — while the none/vllm-simple arms keep full visibility into the same code. That directly undercuts the debuggability goal this same PR's SKILL.md update is pushing for (reading job logs carefully to root-cause failures), even though that SKILL.md section is really about server.log, not the launcher's own xtrace.

Proof, step by step:

  1. Script starts with set -x (line 3) — xtrace is ON.
  2. In the lmcache) arm, after starting the LMCache server in the background, the script calls wait_for_ready --endpoint ... --pid "$LMCACHE_PID" ... as a plain foreground call (line 234).
  3. wait_for_ready()'s first executed line is set +x (benchmark_lib.sh:349) — this runs in the current shell, so xtrace is now OFF for the rest of the script.
  4. The script proceeds to build SPEC_CONFIG, compute MAX_NUM_SEQS=$((2 * CONC)), run the for ((num_seqs=1; ...)) loop building CUDA_GRAPH_CAPTURE_SIZES, and assemble COMPILATION_CONFIG — none of this is traced for the lmcache arm, whereas for none/vllm-simple these same lines run with xtrace still ON (since set -x was never disabled before line 321).
  5. Line 321's { set +x; } 2>/dev/null is now a no-op for the lmcache arm (xtrace was already off) but is the actual first disable point for the other two arms.

Impact is limited to trace verbosity, not execution: the final resolved vLLM command is still written verbatim via printf '%q ' ... | tee "$RESULT_DIR/vllm_command.txt" regardless of xtrace state, and the LMCache command is captured via append_command before the call in question. So the load-bearing debugging artifacts survive; only the intermediate variable-construction trace is lost for this one arm, which is why this is a nit rather than a blocking issue.

Fix is a one-liner: add set -x immediately after the wait_for_ready call in the lmcache arm (or have wait_for_ready save and restore the caller's xtrace state internally, which would also fix this for any future caller).

# same MQ timeout headroom as the MI355X arm.
OFFLOAD_ARGS=(
--kv-transfer-config
"{\"kv_connector\":\"LMCacheMPConnector\",\"kv_connector_module_path\":\"lmcache.integration.vllm.lmcache_mp_connector\",\"kv_role\":\"kv_both\",\"kv_connector_extra_config\":{\"lmcache.mp.port\":$LMCACHE_PORT,\"lmcache.mp.mq_timeout\":6000.0}}"
)
;;
*)
echo "Error: unsupported KV_OFFLOAD_BACKEND='$KV_OFFLOAD_BACKEND' (expected empty or vllm-simple)" >&2
echo "Error: unsupported KV_OFFLOAD_BACKEND='$KV_OFFLOAD_BACKEND' (expected empty, vllm-simple, or lmcache)" >&2
exit 1
;;
esac
Expand Down
27 changes: 27 additions & 0 deletions configs/nvidia-master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1537,6 +1537,33 @@ kimik3-fp4-b300-vllm-agentic-dspark:
# TP8 SimpleCPUOffload (host DRAM)
- { tp: 8, ep: 1, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: vllm-simple }, conc-list: [1, 2, 4, 8, 16] }

# LMCache MP-server DRAM offload on top of the same DSpark MTP serving stack as
# kimik3-fp4-b300-vllm-agentic-dspark (same image, script, and topology). A
# dedicated key so LMCache points can be selected and swept without re-running
# the resident and vllm-simple arms of the base key.
kimik3-fp4-b300-vllm-agentic-dspark-lmcache:
image: vllm/vllm-openai:nightly-b22afe45ac797ae58e67a7a3ad79ee5714024420@sha256:144356af876edbb3a4bfee23e1444b196cc3fdadd0a0c1a7f11f721756972a21
model: moonshotai/Kimi-K3
model-prefix: kimik3
runner: cluster:b300-nv
precision: fp4
framework: vllm
multinode: false
scenarios:
# Agentic-coding only, and 0.63 matches the base key so the LMCache L1 gets
# exactly the host-DRAM budget the SimpleCPUOffload arm gets (~1,889 GB
# aggregate at TP8). The LMCache server runs with --shm-name "" so that L1
# lives in ordinary process memory and is not capped by /dev/shm.
agentic-coding:
- dram-utilization: 0.63
search-space:
# 4/8/16 land on the base key's vllm-simple ladder at its top three
# offload points, so LMCache is directly comparable there; 10 fills the
# 8->16 gap, where the MI355X sister arm's ladder is densest. TP8-only
# for the same reason as the base key: a ~1.5 TB MXFP4 checkpoint does
# not fit below 8 GPUs.
- { tp: 8, ep: 1, spec-decoding: mtp, kv-offloading: dram, kv-offload-backend: { name: lmcache, version: "0.5.4rc2" }, conc-list: [4, 8, 10, 16] }

dsr1-fp8-b200-trt:
image: nvcr.io#nvidia/tensorrt-llm/release:1.3.0rc14
model: deepseek-ai/DeepSeek-R1-0528
Expand Down
9 changes: 9 additions & 0 deletions perf-changelog.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5981,3 +5981,12 @@
- "Inject the committed synthetic MTP acceptance length only for GB300 AgentX throughput; keep eval-only jobs on real target verification."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2571


- config-keys:
- kimik3-fp4-b300-vllm-agentic-dspark-lmcache
scenario-type:
- agentic-coding
description:
- "Add a dedicated LMCache 0.5.4rc2 DRAM KV-offload key at TP8 conc 4/8/10/16 on top of the unchanged kimik3-fp4-b300-vllm-agentic-dspark DSpark MTP stack, with the version pinned in the master config."
- "Run one LMCache MP server per node with --separate-object-groups for the hybrid KDA/MLA multi-group layout, keeping the L1 in process memory (--shm-name \"\") so the DRAM budget is not capped by /dev/shm."
pr-link: https://github.com/SemiAnalysisAI/InferenceX/pull/2597
Loading