Skip to content

Commit d1920cf

Browse files
[JAX] Add an MoE Block (Layer) that compound router, permutation, groupedGEMM and communication (NVIDIA#2912)
Refactor MoEBlock into a unified MoE custom_vjp, add tests Replace the per-primitive custom_vjp boundaries in MoEBlock with a single jax.custom_vjp covering routing, dispatch, expert FFN, and combine. Helper functions group permute -> ragged_all_to_all -> local-permute into a single dispatch / combine pair, with a hand- derived bwd that mirrors the forward and runs entirely inside the EP shard_map body. Add a multi-process (one-GPU-per-process) test suite for the new unified VJP under a 2x2 (ep, fsdp) mesh: * tests/jax/test_multiprocess_moe_vjp.py -- fwd/bwd + aux_loss + PURE_JAX vs TRITON parity at Mixtral-ish shapes (batch=16, seq=2048, hidden=1024, intermediate=4096, num_experts=8, topk=2). * tests/jax/run_multiprocess_moe_vjp.sh -- launcher; forks one pytest process per visible GPU (mirrors examples/jax/encoder/run_test_multiprocessing_encoder.sh). * tests/jax/conftest.py -- pytest --num-process / --process-id options for the launcher. * qa/L0_jax_distributed_unittest/test.sh -- CI hook for the multiprocess smoke. Signed-off-by: tdophung <tdophung@nvidia.com> * [JAX] Fix EP+TRITON combine bwd: save post-A2A expert_outputs Under EP, _combine reassigns expert_outputs locally to the post- ragged_all_to_all tensor before Step 3 (the global combine). Saving the input (pre-A2A) tensor in ctx.expert_outputs meant _combine_bwd's Step-3 inverse (unpermute_bwd_with_merging_probs) consumed a tensor with the wrong shape and contents, silently corrupting d_expert_outputs. _combine now returns (output, expert_outputs_post_ep). The caller stashes the second value as the bwd residual so the Step-3 inverse sees the same tensor the forward Step 3 saw. Signed-off-by: Teddy Do <tdophung@nvidia.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent af5d1e0 commit d1920cf

11 files changed

Lines changed: 4208 additions & 48 deletions

File tree

qa/L0_jax_distributed_unittest/test.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,12 @@ wait
3737
TE_PATH=$TE_PATH bash $TE_PATH/examples/jax/collective_gemm/run_test_cgemm.sh || test_fail "run_test_cgemm.sh"
3838
wait
3939

40+
# MoE custom_vjp distributed suite. Runs one Python process per GPU
41+
# via tests/jax/run_multiprocess_moe_vjp.sh (mirrors the pattern in
42+
# examples/jax/encoder/run_test_multiprocessing_encoder.sh). Requires
43+
# >=4 visible GPUs.
44+
TE_PATH=$TE_PATH bash $TE_PATH/tests/jax/run_multiprocess_moe_vjp.sh \
45+
|| test_fail "test_multiprocess_moe_vjp.py"
4046
# Exercise the multi-GPU tutorial in docs/examples/jax (needs >= 4 GPUs;
4147
# auto-skips otherwise).
4248
CUDA_VISIBLE_DEVICES=0,1,2,3 python3 -m pytest -c $TE_PATH/tests/jax/pytest.ini -v --junitxml=$XML_LOG_DIR/pytest_docs_examples_jax_distributed.xml -k multi_gpu $TE_PATH/docs/examples/jax/ || test_fail "docs/examples/jax (multi-GPU)"

tests/jax/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,20 @@ def pytest_sessionfinish(self, session, exitstatus):
8686
print("=" * 80)
8787

8888

89+
def pytest_addoption(parser):
90+
"""CLI options used by multiprocess JAX tests.
91+
92+
``--num-process`` and ``--process-id`` let a multiprocess launcher
93+
(see ``tests/jax/run_multiprocess_moe_vjp.sh``) fork one pytest
94+
process per GPU and tell each child its rank, so the test module
95+
can call ``jax.distributed.initialize(...)`` with the right
96+
``local_device_ids``. Both default to 0; non-multiprocess tests
97+
ignore them.
98+
"""
99+
parser.addoption("--num-process", action="store", default=0)
100+
parser.addoption("--process-id", action="store", default=0)
101+
102+
89103
def pytest_configure(config):
90104
config.addinivalue_line(
91105
"markers",
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
#!/usr/bin/env bash
2+
# Copyright (c) 2022-2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
#
4+
# See LICENSE for license information.
5+
#
6+
# Multiprocess (one-GPU-per-process) launcher for the unified MoE VJP
7+
# test suite. Forks one pytest invocation per visible GPU, passing each
8+
# its own --num-process=N --process-id=i, and waits for all of them.
9+
# Each child calls jax.distributed.initialize(..., local_device_ids=
10+
# process_id) so each Python process only sees its one GPU as a local
11+
# device and the participating processes form a global mesh.
12+
13+
set -euo pipefail
14+
15+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16+
TE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
17+
TEST_FILE="$TE_ROOT/tests/jax/test_multiprocess_moe_vjp.py"
18+
PYTEST_INI="$TE_ROOT/tests/jax/pytest.ini"
19+
20+
NUM_GPUS="${NUM_GPUS:-$(nvidia-smi -L | wc -l)}"
21+
if [ "$NUM_GPUS" -lt 4 ]; then
22+
echo "[run_multiprocess_moe_vjp.sh] need >=4 GPUs (got $NUM_GPUS); aborting" >&2
23+
exit 1
24+
fi
25+
26+
export XLA_PYTHON_CLIENT_PREALLOCATE="${XLA_PYTHON_CLIENT_PREALLOCATE:-false}"
27+
export XLA_PYTHON_CLIENT_MEM_FRACTION="${XLA_PYTHON_CLIENT_MEM_FRACTION:-0.5}"
28+
export MOE_VJP_COORDINATOR_ADDRESS="${MOE_VJP_COORDINATOR_ADDRESS:-127.0.0.1:13456}"
29+
30+
echo "============================================================"
31+
echo "MoE VJP MULTIPROCESS test (one process per GPU, ${NUM_GPUS} GPUs)"
32+
echo " test file : $TEST_FILE"
33+
echo " coordinator : $MOE_VJP_COORDINATOR_ADDRESS"
34+
echo " XLA_PYTHON_CLIENT_PREALLOCATE: $XLA_PYTHON_CLIENT_PREALLOCATE"
35+
echo " XLA_PYTHON_CLIENT_MEM_FRACTION: $XLA_PYTHON_CLIENT_MEM_FRACTION"
36+
echo "============================================================"
37+
38+
# Per-process logs. MOE_VJP_MP_LOG_DIR can be set to a host-mounted dir
39+
# (e.g. when running inside a container that throws away /tmp on exit)
40+
# so logs survive for postmortem inspection. Defaults to a fresh /tmp.
41+
if [ -n "${MOE_VJP_MP_LOG_DIR:-}" ]; then
42+
LOG_DIR="$MOE_VJP_MP_LOG_DIR"
43+
mkdir -p "$LOG_DIR"
44+
else
45+
LOG_DIR=$(mktemp -d -t moe_vjp_mp_XXXXXX)
46+
fi
47+
echo "Per-process logs: $LOG_DIR"
48+
49+
PIDS=()
50+
51+
cleanup() {
52+
for pid in "${PIDS[@]:-}"; do
53+
if kill -0 "$pid" 2>/dev/null; then
54+
kill -TERM "$pid" 2>/dev/null || true
55+
fi
56+
done
57+
sleep 1
58+
for pid in "${PIDS[@]:-}"; do
59+
if kill -0 "$pid" 2>/dev/null; then
60+
kill -KILL "$pid" 2>/dev/null || true
61+
fi
62+
done
63+
}
64+
trap cleanup EXIT INT TERM
65+
66+
# Launch one pytest per GPU. Process 0 streams to stdout; others log
67+
# only to file so the live output isn't a mosaic.
68+
for i in $(seq 0 $((NUM_GPUS - 1))); do
69+
LOG_FILE="$LOG_DIR/proc_${i}.log"
70+
PYTEST_CMD=(
71+
python3 -m pytest -c "$PYTEST_INI"
72+
"$TEST_FILE"
73+
-p no:typeguard
74+
-v -s
75+
--num-process="$NUM_GPUS"
76+
--process-id="$i"
77+
)
78+
if [ "$i" -eq 0 ]; then
79+
echo "=== Live output from process 0 ==="
80+
"${PYTEST_CMD[@]}" 2>&1 | tee "$LOG_FILE" &
81+
else
82+
"${PYTEST_CMD[@]}" > "$LOG_FILE" 2>&1 &
83+
fi
84+
PIDS+=("$!")
85+
done
86+
87+
# Wait for all and collect exit codes.
88+
EXITS=()
89+
for pid in "${PIDS[@]}"; do
90+
if wait "$pid"; then
91+
EXITS+=("0")
92+
else
93+
EXITS+=("$?")
94+
fi
95+
done
96+
97+
# Summary.
98+
echo
99+
echo "============================================================"
100+
echo "Per-process exit codes:"
101+
for i in "${!EXITS[@]}"; do
102+
echo " proc $i -> ${EXITS[$i]}"
103+
done
104+
105+
# Final pass/fail. Any non-zero in any process fails the suite, but
106+
# we tolerate non-zero on the non-zero processes only if proc 0
107+
# reports PASS (this matches the encoder launcher's logic). Simplest
108+
# Treat exit 0 (pass) and exit 5 (pytest "no tests collected", which
109+
# the file emits via ``pytest.skip(allow_module_level=True)`` on
110+
# pre-Blackwell GPUs) as success. Anything else is a failure.
111+
FAILED=0
112+
for e in "${EXITS[@]}"; do
113+
if [ "$e" != "0" ] && [ "$e" != "5" ]; then
114+
FAILED=1
115+
break
116+
fi
117+
done
118+
119+
echo
120+
if [ "$FAILED" -eq 0 ]; then
121+
echo "[run_multiprocess_moe_vjp.sh] all processes PASSED"
122+
if [ -z "${MOE_VJP_MP_LOG_DIR:-}" ]; then
123+
rm -rf "$LOG_DIR"
124+
fi
125+
exit 0
126+
fi
127+
128+
echo "[run_multiprocess_moe_vjp.sh] at least one process FAILED"
129+
echo " retaining logs at $LOG_DIR for diagnosis"
130+
echo " process 0 tail:"
131+
tail -20 "$LOG_DIR/proc_0.log" 2>/dev/null || true
132+
exit 1

0 commit comments

Comments
 (0)