Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ jobs:
if curl -L -fs -o artifact.json -H "Accept: application/vnd.github+json" -H "X-GitHub-Api-Version: 2022-11-28" https://api.github.com/repos/${{ github.repository }}/actions/artifacts?per_page=100; then
artifact_id=""
if jq empty artifact.json >/dev/null 2>&1; then
artifact_id=$(jq -r ".artifacts[] | select(.name | contains(\"py312\") ) | select(.name | contains(\"mooncake\") ) | select(.name | contains(\"cu130\") | not) | select(.workflow_run.head_sha == \"$SHA\" ) | .id" artifact.json | head -n 1)
artifact_id=$(jq -r ".artifacts[] | select(.name | contains(\"py312\") ) | select(.name | contains(\"mooncake\") ) | select(.name | contains(\"cu130\") ) | select(.workflow_run.head_sha == \"$SHA\" ) | .id" artifact.json | head -n 1)
else
echo "Failed to download artifact list. Retrying..."
fi
Expand Down
95 changes: 93 additions & 2 deletions scripts/tone_tests/scripts/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ get_whl(){

echo "get whl file from github action"
rm -f "$whls_path/mooncake.zip"
rm -f "$whls_path/*.whl"
rm -f "$whls_path"/*.whl

local max_retries=5
local base_delay=5 # seconds
Expand Down Expand Up @@ -401,6 +401,81 @@ cleanup_test_env() {
echo "Cleanup completed"
}

# Wait until GPU memory on the local host drains below a threshold.
# Returns 0 once drained, 1 if it times out.
wait_gpu_idle() {
local max_seconds=${1:-90}
local threshold_mb=${2:-1024}

if ! command -v nvidia-smi >/dev/null 2>&1; then
echo "nvidia-smi not available, skipping GPU drain wait"
return 0
fi

echo "Waiting for GPU memory to drain (threshold ${threshold_mb}MB, timeout ${max_seconds}s)..."
local elapsed=0
local max_used=0
while [ $elapsed -lt $max_seconds ]; do
max_used=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits 2>/dev/null | sort -n | tail -n 1)
[ -z "$max_used" ] && { echo "nvidia-smi query failed, skipping GPU drain wait"; return 0; }
if [ "$max_used" -le "$threshold_mb" ]; then
echo "GPU memory drained (max used ${max_used}MB)"
return 0
fi
sleep 3
elapsed=$((elapsed + 3))
done
echo "GPU memory not drained within ${max_seconds}s (max used ${max_used}MB)"
return 1
}

# Force-kill every host process still holding GPU memory. This catches leftovers

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new GPU drain helpers added around wait_gpu_idle(), force_kill_gpu_procs(), and drain_gpu_local()/drain_gpu_between_tests in scripts/tone_tests/scripts/common.sh look like they assume the CI node is fully dedicated to Mooncake tests, but the host-wide kill behavior is pretty aggressive.

In drain_gpu_local(), if GPU memory has not dropped below the threshold after a container restart, the code calls force_kill_gpu_procs(), which uses nvidia-smi --query-compute-apps=pid to enumerate all GPU-holding PIDs on the host and then sends kill -9 to each of them. drain_gpu_between_tests() calls drain_gpu_local for every case in run_all_tests(), and also invokes it on the remote node via SSH.

On a shared CI host or any environment where other GPU workloads run alongside tone_tests, this will repeatedly and silently terminate unrelated jobs (training runs, other services, etc.), which is a pretty big safety/availability risk. There’s no scoping to the Mooncake container, user, or any job-specific tag; the only filter is "uses GPU compute".

If the operational assumption really is "these nodes are single-tenant and reserved for Mooncake CI", it would be helpful to either codify that constraint or tighten the kill logic so it only targets processes launched by this test harness (e.g., via container PID namespace, cgroups, or a known user). Otherwise, it might be safer to treat "GPU did not drain within timeout" as a hard failure for the suite rather than attempting to clear arbitrary GPU workloads on the host.


🤖 Generated by QoderFix in Qoder

# that an in-container pkill cannot reach: processes reparented to the host
# (orphans) or in a different PID namespace. Only GPU-holding PIDs are targeted.
force_kill_gpu_procs() {
command -v nvidia-smi >/dev/null 2>&1 || return 0
local pids
pids=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader 2>/dev/null | tr -cd '0-9\n' | grep -E '^[0-9]+$' | sort -u)

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.

high

Using tr -cd '0-9\n' to strip all non-digit characters from the entire output of nvidia-smi is highly risky. If nvidia-smi outputs any warning or informational messages containing numbers (such as driver versions, GPU temperatures, or bus IDs), those numbers will be mangled into digits, matched as valid PIDs, and subsequently force-killed. This can lead to the accidental termination of critical system or unrelated user processes on a shared host.

Using awk to extract only fields that are strictly numeric is a much safer and more robust approach.

Suggested change
pids=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader 2>/dev/null | tr -cd '0-9\n' | grep -E '^[0-9]+$' | sort -u)
pids=$(nvidia-smi --query-compute-apps=pid --format=csv,noheader 2>/dev/null | awk '$1 ~ /^[0-9]+$/ {print $1}' | sort -u)

[ -z "$pids" ] && return 0
echo "Force-killing GPU-holding PIDs: $(echo $pids | tr '\n' ' ')"
for pid in $pids; do
kill -9 "$pid" 2>/dev/null || true
done
sleep 3
}

# Fully clear GPU memory on the current node: graceful in-container kill first,
# then host-level force-kill of any process still holding GPU memory.
# Restart the reused container to reset all in-container state (processes, GPU
# memory, ERDMA queue-pairs / RDMA contexts). 'docker restart' keeps the
# writable layer, so the mooncake wheel and ERDMA drivers are NOT reinstalled.
# force_kill_gpu_procs stays as a fallback for leftovers restart cannot reclaim.
drain_gpu_local() {
echo "Restarting container ${CONTAINER_NAME} to reset GPU/ERDMA state..."
docker restart ${CONTAINER_NAME} >/dev/null 2>&1 || true
if ! wait_gpu_idle 60; then
force_kill_gpu_procs
wait_gpu_idle 45 || echo "WARNING: GPU still occupied after restart+force-kill (possible stuck/zombie process or GPU fault)"
fi
}

# Between test cases in run-all the container is reused; reset in-container
# state on both the local and (for double-machine runs) remote nodes via a
# lightweight container restart (no wheel / ERDMA driver reinstall).
drain_gpu_between_tests() {
echo "===== Resetting environment between test cases ====="
drain_gpu_local

if [ -n "$REMOTE_IP" ]; then
echo "Resetting environment on remote node $REMOTE_IP..."
${SSH_CMD} "$REMOTE_IP" "
source ${REMOTE_TEST_DIR}/run/.shrc && \
source ${REMOTE_TEST_DIR}/scripts/common.sh && \
drain_gpu_local
" 2>/dev/null || true
fi
}

setup_node_env() {
local registry_addr=$1
echo "===== Setting up docker environment ====="
Expand All @@ -416,6 +491,7 @@ setup_node_env() {
fi

local extra_args=""
extra_args="$extra_args -e NCCL_GIN_TYPE=0 "
extra_args="$extra_args --device=/dev/infiniband/uverbs0 --device=/dev/infiniband/uverbs1 --device=/dev/infiniband/rdma_cm "
if [ "${USE_HUGGINGFACE_MIRROR}" = "true" ]; then
extra_args="$extra_args -e HF_ENDPOINT=${HUGGINGFACE_MIRROR} -e HF_HUB_ENABLE_HF_TRANSFER=1"
Expand Down Expand Up @@ -840,6 +916,19 @@ collect_and_validate_model_results() {
fi
}

# Echo an offline env prefix when the given model already exists in the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The HuggingFace offline helper and its call sites look generally safe, but there are a couple of behavioral edge cases worth tightening or at least documenting.

hf_offline_prefix() in scripts/tone_tests/scripts/common.sh builds a cache_dir name of models--<model> and then checks for /root/.cache/huggingface/hub/${cache_dir}/snapshots/*/config.json via docker_exec; if that file exists, it prints HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 . The prefix is then injected into:

  • launch_sglang_server() (as ${offline_prefix}python -m sglang.launch_server ...),
  • launch_vllm_server() (appended to env_prefix), and
  • pytest calls in test_moe_mooncake.sh and test_disaggregation_different_tp.sh.

This is good in that offline mode is only enabled when a cache snapshot is detected, but it assumes that:

  1. The HF cache layout always uses models--<name>/snapshots/*/config.json, and
  2. The presence of config.json implies a fully usable snapshot (weights, tokenizer, etc.).

If the cache structure changes, or if a partial snapshot exists (config.json present but other files missing), we might flip into offline mode and end up with non-obvious startup failures that are hard to debug from CI logs. On the flip side, if the snapshot is valid but located differently (or config.json is absent for some reason), the tests will unexpectedly go online again and hit the hub/mirror, reintroducing the 429 risk this PR is trying to avoid.

Would it make sense to either (a) harden the detection (e.g., also checking for weights or a known file) and log when offline mode is enabled, or (b) treat missing cache as a more explicit failure for these tone tests so we don’t silently flip back to online behavior?


🤖 Generated by QoderFix in Qoder

# container's HuggingFace cache, so servers use the local snapshot instead of
# querying the hub (skips downloads and avoids hf-mirror 429 rate limiting).
# Models are pre-cached under MODEL_CACHE (mounted at /root/.cache) on both nodes.
hf_offline_prefix() {
local model_name=$1
[ -z "$model_name" ] && return 0
local cache_dir="models--$(echo "$model_name" | sed 's#/#--#g')"
if ${docker_exec} "ls /root/.cache/huggingface/hub/${cache_dir}/snapshots/*/config.json >/dev/null 2>&1"; then
echo "HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 "
fi
}

launch_sglang_server() {
local model_path=$1
local host=$2
Expand All @@ -855,7 +944,8 @@ launch_sglang_server() {
return 1
fi

local sglang_cmd="${docker_exec} \"python -m sglang.launch_server --model-path ${model_path} --host ${host} --port ${port}"
local offline_prefix=$(hf_offline_prefix "$model_path")
local sglang_cmd="${docker_exec} \"${offline_prefix}python -m sglang.launch_server --model-path ${model_path} --host ${host} --port ${port}"
if [ -n "$extra_args" ]; then
sglang_cmd="${sglang_cmd} ${extra_args}"
fi
Expand Down Expand Up @@ -905,6 +995,7 @@ launch_vllm_server() {
if [ -n "$env_vars" ]; then
env_prefix="${env_vars} "
fi
env_prefix="${env_prefix}$(hf_offline_prefix "$model_path")"

local vllm_cmd="${docker_exec} \"${env_prefix}python3 -m vllm.entrypoints.openai.api_server --model '${model_path}' --host '${host}' --port ${port}"

Expand Down
9 changes: 8 additions & 1 deletion scripts/tone_tests/scripts/run_test.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,10 @@ run_single_test(){
source "$RUN_DIR/.shrc"
cd "$TONE_TESTS_DIR/scripts"
source "./$test_name"


local log_dir="${BASE_DIR}/run/logs/$(basename "$test_name" .sh)"
setup_log_directory "$log_dir"

local exit_code=0
run_test "$@" || exit_code=1

Expand Down Expand Up @@ -282,6 +285,10 @@ run_all_tests(){
fi

[ $exit_code -ne 0 ] && all_passed=false

# Container is shared across cases in run-all; drain leftover GPU
# memory before the next case so it does not OOM / get SIGKILLed.
drain_gpu_between_tests
done

cleanup_test_env "double"
Expand Down
13 changes: 11 additions & 2 deletions scripts/tone_tests/scripts/test_disaggregation_different_tp.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,20 @@ run_test()
local log_file="${BASE_DIR}/${TEST_CASE_RESULT_PATH}/${test_case_name}.log"

echo "Running tests in container and saving output to: $log_file"

# Use local HF cache (offline) only when both models are already downloaded.
local off_mla=$(hf_offline_prefix "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct")
local off_llama=$(hf_offline_prefix "meta-llama/Llama-3.2-3B-Instruct")
local offline_prefix=""
if [ -n "$off_mla" ] && [ -n "$off_llama" ]; then
offline_prefix="$off_mla"
fi

${docker_exec} "\
cd /sgl-workspace/sglang/test/registered/distributed && \
cd /sgl-workspace/sglang/test/registered/disaggregation && \
sed -i '0,/^class /s|^class |DEFAULT_MODEL_NAME_FOR_TEST_MLA = \"deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct\"\nDEFAULT_MODEL_NAME_FOR_TEST = \"meta-llama/Llama-3.2-3B-Instruct\"\n&|' test_disaggregation_different_tp.py && \
echo 'Model override applied successfully' && \
python3 -m pytest test_disaggregation_different_tp.py -v -s --tb=long" | tee "$log_file"
${offline_prefix}python3 -m pytest test_disaggregation_different_tp.py -v -s --tb=long" | tee "$log_file"

return ${PIPESTATUS[0]}
}
Expand Down
7 changes: 5 additions & 2 deletions scripts/tone_tests/scripts/test_moe_mooncake.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ run_test()
local log_file="${BASE_DIR}/${TEST_CASE_RESULT_PATH}/${test_case_name}.log"

echo "Running tests in container and saving output to: $log_file"


# Use local HF cache (offline) when the model is already downloaded.
local offline_prefix=$(hf_offline_prefix "deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct")

${docker_exec} "\
export PYTHONPATH=/sgl-workspace/sglang:\$PYTHONPATH && \
cd /test_run/python && \
python3 -m pytest test_moe_mooncake.py -v -s --tb=long" | tee "$log_file"
${offline_prefix}python3 -m pytest test_moe_mooncake.py -v -s --tb=long" | tee "$log_file"

return ${PIPESTATUS[0]}
}
Expand Down
6 changes: 3 additions & 3 deletions scripts/tone_tests/scripts/test_vllm_1p1d_erdma.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ start_server()

local kv_config_json="{\\\"kv_connector\\\":\\\"MooncakeConnector\\\",\\\"kv_role\\\":\\\"$kv_role\\\"}"

local extra_args="--tensor-parallel-size 2 --max-model-len 32768 --no-enable-prefix-caching --kv-transfer-config '$kv_config_json'"
local extra_args="--tensor-parallel-size 2 --max-model-len 32768 --gpu-memory-utilization 0.85 --no-enable-prefix-caching --kv-transfer-config '$kv_config_json'"

local env_vars="CUDA_VISIBLE_DEVICES=0,1"
local env_vars="CUDA_VISIBLE_DEVICES=6,7"

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.

medium

Hardcoding CUDA_VISIBLE_DEVICES=6,7 makes the test suite non-portable and prone to failure on environments with fewer than 8 GPUs, or where those specific GPUs are already occupied.

Consider allowing this to be overridden via an environment variable, defaulting to 6,7 if not specified.

Suggested change
local env_vars="CUDA_VISIBLE_DEVICES=6,7"
local env_vars="CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-6,7}"


if ! launch_vllm_server "$model_name" "$host" "$port" "$vllm_server_log_path" "$kv_role" "$extra_args" "$env_vars"; then
return 1
Expand All @@ -62,7 +62,7 @@ run_proxy()
local use_health_check=0
if python3 -c "from packaging import version; import sys; sys.exit(0 if version.parse('$vllm_version') >= version.parse('0.16.0') else 1)" 2>/dev/null; then
echo "Using Mooncake Connector Proxy (vLLM >= 0.16.0)"
proxy_script="python3 -u /vllm-workspace/examples/online_serving/disaggregated_serving/mooncake_connector/mooncake_connector_proxy.py --prefill http://$REMOTE_IP:8010 --decode http://$LOCAL_IP:8020 --host 0.0.0.0 --port 8000"
proxy_script="python3 -u /vllm-workspace/examples/disaggregated/mooncake_connector/mooncake_connector_proxy.py --prefill http://$REMOTE_IP:8010 --decode http://$LOCAL_IP:8020 --host 0.0.0.0 --port 8000"
ready_pattern="All prefiller instances are ready."
else
echo "Using NIXL Proxy (vLLM < 0.16.0)"
Expand Down
Loading