Skip to content

Commit 87541d3

Browse files
author
ssjia
committed
Update (base update)
[ghstack-poisoned]
2 parents d3cf022 + 1621fa2 commit 87541d3

527 files changed

Lines changed: 24135 additions & 6083 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ci/scripts/export_model_artifact.sh

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -422,8 +422,9 @@ if [ "$MODEL_NAME" = "qwen3_5_moe" ]; then
422422
--no-compile
423423
echo "::endgroup::"
424424

425-
# Copy tokenizer for the runner
425+
# Copy tokenizer files for the runner and model-specific serving launcher.
426426
cp "$LOCAL_MODEL_DIR/tokenizer.json" "${OUTPUT_DIR}/tokenizer.json"
427+
cp "$LOCAL_MODEL_DIR/tokenizer_config.json" "${OUTPUT_DIR}/tokenizer_config.json"
427428

428429
# Export to .pte/.ptd (short cache dir avoids objcopy symbol length issues)
429430
echo "::group::Export"

.ci/scripts/test_model_e2e.sh

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,4 +447,105 @@ case "$MODEL_NAME" in
447447
esac
448448
echo "::endgroup::"
449449

450+
if [ "$DEVICE" = "cuda" ] && [ "$MODEL_NAME" = "qwen3_5_moe" ]; then
451+
echo "::group::Run $MODEL_NAME OpenAI serving smoke"
452+
pip install -r examples/llm_server/python/requirements.txt "transformers==5.0.0rc1"
453+
python -m pip install --no-deps --no-build-isolation --editable . -v
454+
455+
PORT=$(python - <<'PY'
456+
import socket
457+
458+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
459+
s.bind(("127.0.0.1", 0))
460+
print(s.getsockname()[1])
461+
PY
462+
)
463+
SERVER_LOG=$(mktemp)
464+
WORKER_BIN="cmake-out/examples/models/qwen3_5_moe/qwen3_5_moe_worker"
465+
python -u -m executorch.examples.models.qwen3_5_moe.serve \
466+
--model-path "${MODEL_DIR}/model.pte" \
467+
--data-path "${MODEL_DIR}/aoti_cuda_blob.ptd" \
468+
--tokenizer-path "${MODEL_DIR}/tokenizer.json" \
469+
--hf-tokenizer "${MODEL_DIR}" \
470+
--model-id qwen3.5-moe \
471+
--max-context 4096 \
472+
--max-sessions 2 \
473+
--no-think \
474+
--worker-bin "$WORKER_BIN" \
475+
--host 127.0.0.1 \
476+
--port "$PORT" >"$SERVER_LOG" 2>&1 &
477+
SERVER_PID=$!
478+
479+
cleanup_qwen_server() {
480+
if kill -0 "$SERVER_PID" 2>/dev/null; then
481+
kill "$SERVER_PID" 2>/dev/null || true
482+
wait "$SERVER_PID" 2>/dev/null || true
483+
fi
484+
rm -f "$SERVER_LOG"
485+
}
486+
trap cleanup_qwen_server EXIT
487+
488+
if ! python - "$PORT" "$SERVER_LOG" <<'PY'
489+
import json
490+
import sys
491+
import time
492+
import urllib.request
493+
494+
port = sys.argv[1]
495+
log_path = sys.argv[2]
496+
base = f"http://127.0.0.1:{port}"
497+
498+
499+
def request(path, payload=None):
500+
data = None
501+
headers = {}
502+
if payload is not None:
503+
data = json.dumps(payload).encode("utf-8")
504+
headers["Content-Type"] = "application/json"
505+
req = urllib.request.Request(base + path, data=data, headers=headers)
506+
with urllib.request.urlopen(req, timeout=120) as resp:
507+
return json.loads(resp.read().decode("utf-8"))
508+
509+
510+
last = None
511+
for _ in range(180):
512+
try:
513+
request("/health")
514+
break
515+
except Exception as e:
516+
last = e
517+
time.sleep(1)
518+
else:
519+
print(open(log_path, encoding="utf-8", errors="replace").read())
520+
raise RuntimeError(f"server did not become healthy: {last}")
521+
522+
models = request("/v1/models")
523+
ids = {m["id"] for m in models["data"]}
524+
if "qwen3.5-moe" not in ids:
525+
raise AssertionError(f"qwen3.5-moe missing from /v1/models: {ids}")
526+
527+
body = {
528+
"model": "qwen3.5-moe",
529+
"messages": [{"role": "user", "content": "What is the capital of France?"}],
530+
"max_tokens": 32,
531+
"temperature": 0,
532+
}
533+
resp = request("/v1/chat/completions", body)
534+
content = resp["choices"][0]["message"].get("content") or ""
535+
if "Paris" not in content:
536+
raise AssertionError(f"expected Paris in serving response, got: {content!r}")
537+
538+
print("Qwen3.5-MoE serving smoke passed")
539+
PY
540+
then
541+
echo "Qwen3.5-MoE serving smoke failed; server log:"
542+
cat "$SERVER_LOG"
543+
exit 1
544+
fi
545+
546+
cleanup_qwen_server
547+
trap - EXIT
548+
echo "::endgroup::"
549+
fi
550+
450551
popd

.claude/skills/qualcomm/new_op_development.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,17 @@ class DecomposeMyOp(ExportPass):
217217

218218
### Registration (all decompose passes)
219219
1. `_passes/__init__.py` — import + `__all__`
220-
2. `_passes/qnn_pass_manager.py` — import + `transform_for_annotation_pipeline` + `transform_for_export_pipeline` + `get_capture_program_passes`
221-
3. `_passes/utils.py` — add to `get_passes_dependency_for_capture_program()` with `[RemoveRedundancy]` dependency
220+
2. `_passes/qnn_pass_manager.py` — The pass manager uses classmethods for pipeline definitions:
221+
- **Import** — add to the import block at top of file
222+
- **`get_annotation_passes()`** — add pass class to the returned list (runs before quantizer, ATen IR)
223+
- **`get_export_passes()`** — add pass class if needed for float-only path (runs after quantization, before to-edge)
224+
- **`get_default_pass_activations()`** — add `(PassClass, True)` ONLY if the pass also needs to run in the to-edge pipeline
225+
- **`get_passes_dependency_for_capture_program()`** — add `PassClass: [RemoveRedundancy]` dependency ONLY if also in `get_default_pass_activations`
226+
227+
**When to add to which pipeline:**
228+
- **Annotation only** (most common for decompose passes): `get_annotation_passes()` — pass decomposes the op before the quantizer sees it
229+
- **Export pipeline** too: if the float-only test fails without it (op doesn't get handled by PyTorch's built-in decomposition during to-edge)
230+
- **Capture program** (to-edge) too: if the op can appear in edge dialect and needs decomposition there (e.g., `DecomposeVar`, `DecomposeCDist`, `DecomposeDiagonal`)
222231

223232
---
224233

@@ -255,4 +264,4 @@ class DecomposeMyOp(ExportPass):
255264

256265
**Native QNN Op:** `qnn_constants.py``op_my_op.py``builders/__init__.py``htp_rules.py``lpai_rules.py``layout_transform.py``tests/models.py``test_qnn_delegate.py``partition/utils.py` (skip decomp) → `common_defs.py` (remove to_be_implemented) → `builders/README.md`
257266

258-
**Decompose Pass:** `_passes/decompose_my_op.py``_passes/__init__.py``qnn_pass_manager.py` (annotation + export + capture) → `_passes/utils.py` (dependency) → `tests/models.py``test_qnn_delegate.py``common_defs.py``builders/README.md`
267+
**Decompose Pass:** `_passes/decompose_my_op.py``_passes/__init__.py``qnn_pass_manager.py` (`get_annotation_passes` + optionally `get_export_passes`; if also needed in to-edge: `get_default_pass_activations` + `get_passes_dependency_for_capture_program`) → `tests/models.py``test_qnn_delegate.py``common_defs.py``builders/README.md`

.github/workflows/build-cadence-runner.yml

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,36 +19,18 @@ concurrency:
1919
cancel-in-progress: true
2020

2121
jobs:
22-
gate:
23-
runs-on: ubuntu-latest
24-
outputs:
25-
run-cadence: ${{ steps.decide.outputs.run }}
26-
steps:
27-
- id: decide
28-
env:
29-
EVENT: ${{ github.event_name }}
30-
IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
31-
HAS_CLA: ${{ contains(github.event.pull_request.labels.*.name, 'CLA Signed') }}
32-
HAS_EXPORT: ${{ contains(github.event.pull_request.labels.*.name, 'meta-exported') }}
33-
run: |
34-
run=false
35-
case "${EVENT}" in
36-
push|schedule|workflow_dispatch)
37-
run=true
38-
;;
39-
pull_request)
40-
[ "${IS_FORK}" = "false" ] && run=true
41-
;;
42-
pull_request_target)
43-
if [ "${IS_FORK}" = "true" ] && [ "${HAS_CLA}" = "true" ] && [ "${HAS_EXPORT}" = "true" ]; then
44-
run=true
45-
fi
46-
;;
47-
esac
48-
echo "run=${run}" >> "${GITHUB_OUTPUT}"
49-
22+
# Same-repo PRs run on pull_request, which reads the PR's own workflow AND code
23+
# -- so CI changes, new test jobs, code, and tests are all validated pre-merge.
24+
# Fork PRs can't get credentials (OIDC) on pull_request, so Meta-exported forks
25+
# (labeled CLA Signed + meta-exported) run on pull_request_target instead. The
26+
# run condition is inlined per job (GitHub Actions has no YAML anchors and env
27+
# is unavailable in job-level if), so keep the copies in sync.
5028
cpu-build:
51-
if: github.event_name != 'pull_request_target'
29+
if: >-
30+
github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ||
31+
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
32+
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository &&
33+
contains(github.event.pull_request.labels.*.name, 'CLA Signed') && contains(github.event.pull_request.labels.*.name, 'meta-exported'))
5234
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
5335
permissions:
5436
id-token: write
@@ -58,7 +40,7 @@ jobs:
5840
runner: linux.2xlarge
5941
docker-image: ci-image:executorch-ubuntu-22.04-clang12
6042
submodules: recursive
61-
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
43+
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}
6244
timeout: 90
6345
upload-artifact: cadence-runner-build
6446
script: |
@@ -75,21 +57,28 @@ jobs:
7557
7658
cpu-test:
7759
needs: cpu-build
78-
if: github.event_name != 'pull_request_target'
60+
if: >-
61+
github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ||
62+
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
63+
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository &&
64+
contains(github.event.pull_request.labels.*.name, 'CLA Signed') && contains(github.event.pull_request.labels.*.name, 'meta-exported'))
7965
permissions:
8066
id-token: write
8167
contents: read
8268
uses: ./.github/workflows/_test_cadence.yml
8369
with:
84-
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
70+
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}
8571

8672
# Cross-compile cadence_executor_runner for each Cadence Xtensa core, one job
8773
# per backend so they show as separate lines (no matrix grouping). Shared logic
8874
# lives in _xtensa_build.yml. fusion_g3 is omitted until the upstream fusion_g3
8975
# <-> nnlib-FusionG3 API skew is fixed (its runner does not link).
9076
hifi-build:
91-
needs: gate
92-
if: needs.gate.outputs.run-cadence == 'true'
77+
if: >-
78+
github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ||
79+
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
80+
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository &&
81+
contains(github.event.pull_request.labels.*.name, 'CLA Signed') && contains(github.event.pull_request.labels.*.name, 'meta-exported'))
9382
permissions:
9483
id-token: write
9584
contents: read
@@ -99,8 +88,11 @@ jobs:
9988
ref: ${{ (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') && github.event.pull_request.head.sha || github.sha }}
10089

10190
vision-build:
102-
needs: gate
103-
if: needs.gate.outputs.run-cadence == 'true'
91+
if: >-
92+
github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' ||
93+
(github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository) ||
94+
(github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name != github.repository &&
95+
contains(github.event.pull_request.labels.*.name, 'CLA Signed') && contains(github.event.pull_request.labels.*.name, 'meta-exported'))
10496
permissions:
10597
id-token: write
10698
contents: read

.github/workflows/mlx.yml

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,11 @@ jobs:
6666
echo "::endgroup::"
6767
6868
echo "::group::Build test runners"
69-
${CONDA_RUN} cmake --build cmake-out --target op_test_runner multi_thread_test_runner -j$(( $(sysctl -n hw.ncpu) - 1 ))
69+
${CONDA_RUN} cmake --build cmake-out --target op_test_runner multi_thread_test_runner mlx_mutable_state_test -j$(( $(sysctl -n hw.ncpu) - 1 ))
70+
echo "::endgroup::"
71+
72+
echo "::group::Run mutable-state (multi-session) unit test"
73+
./cmake-out/backends/mlx/test/mlx_mutable_state_test
7074
echo "::endgroup::"
7175
7276
echo "::group::Run op unit tests"
@@ -161,6 +165,29 @@ jobs:
161165
fi
162166
echo "::endgroup::"
163167
168+
echo "::group::Verify chunked == unchunked prefill"
169+
QWEN_TINY_PTE=/tmp/qwen35_moe_mlx_tiny/model.pte \
170+
${CONDA_RUN} python -m pytest \
171+
examples/models/qwen3_5_moe/test_chunked_prefill.py -v
172+
echo "::endgroup::"
173+
174+
echo "::group::Build Qwen 3.5 MoE MLX C++ runner"
175+
# Validates the MLX C++ runner build wiring (compile + link + metallib).
176+
# The tiny model has no compatible tokenizer (vocab 256, random weights),
177+
# so we don't run C++ inference here — only confirm it builds.
178+
${CONDA_RUN} make qwen3_5_moe-mlx
179+
RUNNER=cmake-out/examples/models/qwen3_5_moe/qwen3_5_moe_runner
180+
if [ ! -x "$RUNNER" ]; then
181+
echo "Failed: runner not found at $RUNNER"
182+
exit 1
183+
fi
184+
if [ ! -f "$(dirname "$RUNNER")/mlx.metallib" ]; then
185+
echo "Failed: mlx.metallib not copied next to runner"
186+
exit 1
187+
fi
188+
echo "Success: built $RUNNER"
189+
echo "::endgroup::"
190+
164191
backend-tester:
165192
needs: run-decision
166193
if: |

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ arm-scratch/
2626
executorch.egg-info
2727
pip-out/
2828
build-profiling/
29+
**/ddr_*_temp
2930

3031
# Any exported models and profiling outputs
3132
*.bin

.lintrunner.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,7 @@ exclude_patterns = [
391391
'**/*.gif',
392392
'extension/llm/tokenizers',
393393
'extension/llm/tokenizers/**',
394+
'examples/llm_server',
394395
'backends/cadence/utils/FACTO',
395396
'examples/cuda',
396397
'examples/qualcomm',

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@
9191
#
9292
# ==============================================================================
9393

94-
.PHONY: voxtral-cuda voxtral-cpu voxtral-metal voxtral-mlx voxtral_realtime-cuda voxtral_realtime-cpu voxtral_realtime-metal voxtral_realtime-mlx voxtral_tts-cpu voxtral_tts-cuda whisper-cuda whisper-cuda-debug whisper-cpu whisper-metal parakeet-cuda parakeet-cuda-debug parakeet-cpu parakeet-metal parakeet-mlx parakeet-vulkan dinov2-cuda dinov2-cuda-debug sortformer-cuda sortformer-cpu silero-vad-cpu llama-cuda llama-cuda-debug llama-cpu lfm_2_5-mlx llava-cpu gemma3-cuda gemma3-cpu gemma4_31b-cuda gemma4_31b-mlx qwen3_5_moe-cuda qwen3_5_moe-metal clean help
94+
.PHONY: voxtral-cuda voxtral-cpu voxtral-metal voxtral-mlx voxtral_realtime-cuda voxtral_realtime-cpu voxtral_realtime-metal voxtral_realtime-mlx voxtral_tts-cpu voxtral_tts-cuda whisper-cuda whisper-cuda-debug whisper-cpu whisper-metal parakeet-cuda parakeet-cuda-debug parakeet-cpu parakeet-metal parakeet-mlx parakeet-vulkan dinov2-cuda dinov2-cuda-debug sortformer-cuda sortformer-cpu silero-vad-cpu llama-cuda llama-cuda-debug llama-cpu lfm_2_5-mlx llava-cpu gemma3-cuda gemma3-cpu gemma4_31b-cuda gemma4_31b-mlx qwen3_5_moe-cuda qwen3_5_moe-metal qwen3_5_moe-mlx clean help
9595

9696
help:
9797
@echo "This Makefile adds targets to build runners for various models on various backends. Run using \`make <target>\`. Available targets:"
@@ -131,6 +131,7 @@ help:
131131
@echo " gemma4_31b-mlx - Build Gemma 4 31B runner with MLX backend"
132132
@echo " qwen3_5_moe-cuda - Build Qwen3.5 MoE runner with CUDA backend"
133133
@echo " qwen3_5_moe-metal - Build Qwen3.5 MoE runner with Metal backend"
134+
@echo " qwen3_5_moe-mlx - Build Qwen3.5 MoE runner with MLX backend"
134135
@echo " clean - Clean build artifacts"
135136

136137
voxtral-cuda:
@@ -467,6 +468,15 @@ qwen3_5_moe-metal:
467468
@echo "✓ Build complete!"
468469
@echo " Binary: cmake-out/examples/models/qwen3_5_moe/qwen3_5_moe_runner"
469470

471+
qwen3_5_moe-mlx:
472+
@echo "==> Building and installing ExecuTorch with MLX..."
473+
cmake --workflow --preset mlx-release
474+
@echo "==> Building Qwen3.5 MoE runner with MLX..."
475+
cd examples/models/qwen3_5_moe && cmake --workflow --preset qwen3-5-moe-mlx
476+
@echo ""
477+
@echo "✓ Build complete!"
478+
@echo " Binary: cmake-out/examples/models/qwen3_5_moe/qwen3_5_moe_runner"
479+
470480
clean:
471481
rm -rf cmake-out \
472482
extension/llm/tokenizers/build \

backends/aoti/aoti_backend.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# LICENSE file in the root directory of this source tree.
66

77
import contextlib
8+
import hashlib
89
import os
910
import typing
1011
from abc import ABC, abstractmethod
@@ -276,18 +277,21 @@ def preprocess(
276277

277278
# Create named data store
278279
named_data_store = NamedDataStore()
279-
method_name = cls.method_name_from_compile_specs(compile_specs)
280280

281-
named_data_store.add_named_data(method_name + "_so_blob", so_data, 1, None)
281+
# Key each blob by a content hash so partitions in one method get distinct
282+
# keys (a method-name-only key collides). Runtime recovers them from
283+
# processed_bytes below.
284+
so_blob_key = hashlib.sha256(so_data).hexdigest() + "_so_blob"
285+
weights_blob_key = hashlib.sha256(blob_data).hexdigest() + "_weights_blob"
286+
287+
named_data_store.add_named_data(so_blob_key, so_data, 1, None)
282288
# Determine whether to save named data externally based on backend setting
283289
# External: save to separate .ptd file, otherwise merge with .pte file
284290
external_tag = (
285291
f"aoti_{device_name}_blob" if cls.save_data_externally() else None
286292
)
287293

288-
named_data_store.add_named_data(
289-
method_name + "_weights_blob", blob_data, 1, external_tag
290-
)
294+
named_data_store.add_named_data(weights_blob_key, blob_data, 1, external_tag)
291295

292296
# Clean up the generated files
293297
os.remove(so_path)
@@ -299,8 +303,11 @@ def preprocess(
299303
# the next preprocess call (e.g. for the next method).
300304
cls.release_moved_tensors(device_edge_program, compile_specs)
301305

306+
# The runtime cannot recompute these hash keys, so carry them (one per line).
307+
processed_bytes = (so_blob_key + "\n" + weights_blob_key).encode("utf-8")
308+
302309
return PreprocessResult(
303-
processed_bytes=b"",
310+
processed_bytes=processed_bytes,
304311
debug_handle_map={},
305312
data_store_output=named_data_store.get_named_data_store_output(),
306313
)

0 commit comments

Comments
 (0)