-
Notifications
You must be signed in to change notification settings - Fork 1k
370 lines (341 loc) · 15.8 KB
/
Copy pathmetal.yml
File metadata and controls
370 lines (341 loc) · 15.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
name: Test Metal Backend
on:
push:
branches:
- main
- release/*
tags:
- ciflow/metal/*
pull_request:
paths:
- .github/workflows/metal.yml
- backends/apple/metal/**
- backends/aoti/**
- examples/models/qwen3_5_moe/**
- extension/llm/export/**
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}-${{ github.event_name == 'workflow_dispatch' }}-${{ github.event_name == 'schedule' }}
cancel-in-progress: true
permissions:
contents: read
jobs:
changed-files:
name: Get changed files
uses: ./.github/workflows/_get-changed-files.yml
with:
include-push-diff: true
run-decision:
name: CI run decision
uses: ./.github/workflows/_ci-run-decision.yml
test-metal-builds:
name: test-executorch-metal-build
needs: [changed-files, run-decision]
# Path-filtered: mirrors the workflow-level pull_request `paths:`
# filter so push commits that don't touch metal-relevant paths skip
# this job on non-sampled commits. See _ci-run-decision.yml for
# the sampling policy.
if: |
contains(needs.changed-files.outputs.changed-files, 'backends/apple/metal') ||
contains(needs.changed-files.outputs.changed-files, 'backends/aoti') ||
contains(needs.changed-files.outputs.changed-files, 'examples/models/qwen3_5_moe') ||
contains(needs.changed-files.outputs.changed-files, 'extension/llm/export') ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/metal.yml') ||
needs.run-decision.outputs.is-full-run == 'true'
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
with:
default-packages: ""
runner: macos-m2-stable
python-version: '3.11'
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: 90
script: |
set -eux
echo "::group::Test ExecuTorch Metal build"
PYTHON_EXECUTABLE=python CMAKE_ARGS="-DEXECUTORCH_BUILD_METAL=ON" ${CONDA_RUN} --no-capture-output ./install_executorch.sh
echo "::endgroup::"
test-metal-modules:
name: test-metal-backend-modules
needs: [changed-files, run-decision]
if: |
contains(needs.changed-files.outputs.changed-files, 'backends/apple/metal') ||
contains(needs.changed-files.outputs.changed-files, 'backends/aoti') ||
contains(needs.changed-files.outputs.changed-files, 'examples/models/qwen3_5_moe') ||
contains(needs.changed-files.outputs.changed-files, 'extension/llm/export') ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/metal.yml') ||
needs.run-decision.outputs.is-full-run == 'true'
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
with:
default-packages: ""
runner: macos-m2-stable
python-version: '3.11'
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: 120
script: |
set -eux
echo "::group::Setup ExecuTorch"
PYTHON_EXECUTABLE=python ${CONDA_RUN} EXECUTORCH_BUILD_KERNELS_TORCHAO=1 TORCHAO_BUILD_EXPERIMENTAL_MPS=1 ./install_executorch.sh
echo "::endgroup::"
echo "::group::Build Metal Runtime"
${CONDA_RUN} backends/apple/metal/tests/run_metal_test.sh --build
echo "::endgroup::"
echo "::group::Run Metal Backend Module Tests"
${CONDA_RUN} python -m unittest backends.apple.metal.tests.test_modules.TestMetalBackendModules
echo "::endgroup::"
test-metal-qwen35-moe-tiny:
name: test-metal-qwen35-moe-tiny
needs: [changed-files, run-decision]
if: |
contains(needs.changed-files.outputs.changed-files, 'backends/apple/metal') ||
contains(needs.changed-files.outputs.changed-files, 'backends/aoti') ||
contains(needs.changed-files.outputs.changed-files, 'examples/models/qwen3_5_moe') ||
contains(needs.changed-files.outputs.changed-files, 'extension/llm/export') ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/metal.yml') ||
needs.run-decision.outputs.is-full-run == 'true'
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
with:
default-packages: ""
runner: macos-m2-stable
python-version: '3.11'
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: 120
script: |
set -eux
echo "::group::Setup ExecuTorch"
PYTHON_EXECUTABLE=python ${CONDA_RUN} EXECUTORCH_BUILD_KERNELS_TORCHAO=1 TORCHAO_BUILD_EXPERIMENTAL_MPS=1 ./install_executorch.sh
echo "::endgroup::"
# Isolate Inductor cache per job to prevent PCH conflicts
export TMPDIR=$(mktemp -d "${RUNNER_TEMP}/tmpdir_XXXXXX")
export TORCHINDUCTOR_CACHE_DIR=$(mktemp -d "${RUNNER_TEMP}/inductor_cache_XXXXXX")
echo "::group::Export Qwen 3.5 MoE (tiny model, Metal)"
${CONDA_RUN} python -m executorch.examples.models.qwen3_5_moe.export \
--tiny-test \
--backend metal \
--qlinear fpa4w \
--output-dir /tmp/qwen35_moe_metal_tiny
echo "::endgroup::"
echo "::group::Build Metal runtime and Qwen 3.5 MoE runner"
${CONDA_RUN} cmake --workflow --preset llm-release-metal
cd examples/models/qwen3_5_moe
${CONDA_RUN} cmake --workflow --preset qwen3-5-moe-metal
cd -
echo "::endgroup::"
# Create a byte-level tokenizer for the tiny model (vocab_size=256).
# Maps each byte value to its own token ID so any prompt produces valid IDs.
${CONDA_RUN} python - <<'PY'
import json
vocab = {chr(i) if 32 <= i < 127 else f'<0x{i:02X}>': i for i in range(256)}
tokenizer = {
'version': '1.0',
'model': {'type': 'BPE', 'vocab': vocab, 'merges': []},
'added_tokens': [{'id': i, 'content': chr(i) if 32 <= i < 127 else f'<0x{i:02X}>', 'single_word': False, 'lstrip': False, 'rstrip': False, 'normalized': False, 'special': False} for i in range(256)],
}
with open('/tmp/qwen35_moe_metal_tiny/tokenizer.json', 'w') as f:
json.dump(tokenizer, f)
print('Created byte-level tokenizer.json')
PY
RUNNER=./cmake-out/examples/models/qwen3_5_moe/qwen3_5_moe_runner
# Patch absolute libomp install name to rpath-based lookup (same as test_model_e2e.sh)
if otool -L "$RUNNER" | grep -q "/opt/llvm-openmp/lib/libomp.dylib"; then
install_name_tool -change /opt/llvm-openmp/lib/libomp.dylib @rpath/libomp.dylib "$RUNNER"
fi
MODEL=/tmp/qwen35_moe_metal_tiny/model.pte
TOKENIZER=/tmp/qwen35_moe_metal_tiny/tokenizer.json
echo "::group::Run Qwen 3.5 MoE inference (T=1 decode)"
# Single-char prompt → 1 token → exercises decode-only path
set +e
OUTPUT=$($RUNNER --model_path $MODEL --tokenizer_path $TOKENIZER \
--prompt "A" --temperature 0 --max_new_tokens 4 2>&1)
RC=$?
set -e
echo "$OUTPUT"
if [ $RC -ne 0 ]; then
echo "Failed: runner exited with code $RC"
exit 1
fi
echo "$OUTPUT" | grep -q "Prompt tokens: 1" || { echo "Failed: expected 1 prompt token for decode path"; exit 1; }
echo "$OUTPUT" | grep -q "Decode:" || { echo "Failed: decode did not complete"; exit 1; }
echo "Success: decode completed"
echo "::endgroup::"
echo "::group::Run Qwen 3.5 MoE inference (T>2 prefill + decode)"
set +e
OUTPUT=$($RUNNER --model_path $MODEL --tokenizer_path $TOKENIZER \
--prompt "one two three" --temperature 0 --max_new_tokens 4 2>&1)
RC=$?
set -e
echo "$OUTPUT"
if [ $RC -ne 0 ]; then
echo "Failed: runner exited with code $RC"
exit 1
fi
# Byte-level tokenizer: "one two three" = 13 tokens (13 bytes)
PROMPT_TOKENS=$(echo "$OUTPUT" | grep -o "Prompt tokens: [0-9]*" | head -1 | grep -o "[0-9]*")
if [ "$PROMPT_TOKENS" -le 2 ]; then
echo "Failed: expected >2 prompt tokens for prefill path, got $PROMPT_TOKENS"
exit 1
fi
echo "$OUTPUT" | grep -q "Decode:" || { echo "Failed: prefill + decode did not complete"; exit 1; }
echo "Success: prefill ($PROMPT_TOKENS tokens) + decode completed"
echo "::endgroup::"
export-model-metal-artifact:
name: export-model-metal-artifact
# Skip this job if the pull request is from a fork (HuggingFace secrets are not available).
# Path-filtered on push: mirrors the workflow-level pull_request `paths:`
# filter so push commits that don't touch metal-relevant paths skip
# this job on non-sampled commits.
needs: [changed-files, run-decision]
if: |
(github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request') &&
(
contains(needs.changed-files.outputs.changed-files, 'backends/apple/metal') ||
contains(needs.changed-files.outputs.changed-files, 'backends/aoti') ||
contains(needs.changed-files.outputs.changed-files, 'examples/models/qwen3_5_moe') ||
contains(needs.changed-files.outputs.changed-files, 'extension/llm/export') ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/metal.yml') ||
needs.run-decision.outputs.is-full-run == 'true'
)
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
secrets: inherit
strategy:
fail-fast: false
matrix:
model:
- repo: "mistralai"
name: "Voxtral-Mini-3B-2507"
- repo: "mistralai"
name: "Voxtral-Mini-4B-Realtime-2602"
- repo: "openai"
name: "whisper-small"
- repo: "openai"
name: "whisper-large-v3-turbo"
- repo: "nvidia"
name: "parakeet-tdt"
quant:
- "non-quantized"
- "quantized-int4-metal"
exclude:
# Exclude non-quantized for Voxtral Realtime (too large)
- model:
repo: "mistralai"
name: "Voxtral-Mini-4B-Realtime-2602"
quant: "non-quantized"
with:
default-packages: ""
runner: macos-m2-stable
python-version: '3.11'
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: 90
secrets-env: EXECUTORCH_HF_TOKEN
upload-artifact: ${{ matrix.model.repo }}-${{ matrix.model.name }}-metal-${{ matrix.quant }}
script: |
set -eux
echo "::group::Setup Huggingface"
${CONDA_RUN} pip install -U "huggingface_hub[cli]<1.0" accelerate
${CONDA_RUN} huggingface-cli login --token $SECRET_EXECUTORCH_HF_TOKEN
echo "::endgroup::"
echo "::group::Setup Optimum-ExecuTorch"
OPTIMUM_ET_VERSION=$(cat .ci/docker/ci_commit_pins/optimum-executorch.txt)
echo "Using optimum-executorch version: ${OPTIMUM_ET_VERSION}"
${CONDA_RUN} pip install git+https://github.com/huggingface/optimum-executorch.git@${OPTIMUM_ET_VERSION}
echo "::endgroup::"
echo "::group::Setup ExecuTorch"
PYTHON_EXECUTABLE=python ${CONDA_RUN} EXECUTORCH_BUILD_KERNELS_TORCHAO=1 TORCHAO_BUILD_EXPERIMENTAL_MPS=1 ./install_executorch.sh
echo "::endgroup::"
echo "::group::Pip List"
${CONDA_RUN} pip list
echo "::endgroup::"
# Isolate Inductor cache and precompiled headers (PCH) per job to prevent
# PCH mtime conflicts between parallel matrix jobs on the same runner.
# TORCHINDUCTOR_CACHE_DIR isolates the code cache; setting TMPDIR isolates
# the PCH dir, which PyTorch derives from tempfile.gettempdir() independently.
export TMPDIR=$(mktemp -d "${RUNNER_TEMP}/tmpdir_XXXXXX")
export TORCHINDUCTOR_CACHE_DIR=$(mktemp -d "${RUNNER_TEMP}/inductor_cache_XXXXXX")
VR_MODE=""
if [ "${{ matrix.model.name }}" = "Voxtral-Mini-4B-Realtime-2602" ]; then
VR_MODE="vr-offline"
fi
${CONDA_RUN} bash .ci/scripts/export_model_artifact.sh metal "${{ matrix.model.repo }}/${{ matrix.model.name }}" "${{ matrix.quant }}" "${RUNNER_ARTIFACT_DIR}" "${VR_MODE}"
test-model-metal-e2e:
name: test-model-metal-e2e
# Same path filter as export-model-metal-artifact above. Also
# explicitly gated on the export job succeeding — when needs: jobs
# are *skipped* (e.g. fork PR), GitHub still evaluates this if:,
# so without the explicit success-check this job would run and then
# fail trying to download an artifact that was never produced.
needs: [changed-files, export-model-metal-artifact, run-decision]
if: |
needs.export-model-metal-artifact.result == 'success' &&
(
contains(needs.changed-files.outputs.changed-files, 'backends/apple/metal') ||
contains(needs.changed-files.outputs.changed-files, 'backends/aoti') ||
contains(needs.changed-files.outputs.changed-files, 'examples/models/qwen3_5_moe') ||
contains(needs.changed-files.outputs.changed-files, 'extension/llm/export') ||
contains(needs.changed-files.outputs.changed-files, '.github/workflows/metal.yml') ||
needs.run-decision.outputs.is-full-run == 'true'
)
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
strategy:
fail-fast: false
matrix:
model:
- repo: "mistralai"
name: "Voxtral-Mini-3B-2507"
- repo: "mistralai"
name: "Voxtral-Mini-4B-Realtime-2602"
- repo: "openai"
name: "whisper-small"
- repo: "openai"
name: "whisper-large-v3-turbo"
- repo: "nvidia"
name: "parakeet-tdt"
quant:
- "non-quantized"
- "quantized-int4-metal"
exclude:
# Exclude non-quantized for Voxtral Realtime (too large)
- model:
repo: "mistralai"
name: "Voxtral-Mini-4B-Realtime-2602"
quant: "non-quantized"
with:
default-packages: ""
runner: macos-m2-stable
python-version: '3.11'
submodules: 'recursive'
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
timeout: 90
download-artifact: ${{ matrix.model.repo }}-${{ matrix.model.name }}-metal-${{ matrix.quant }}
script: |
set -eux
echo "::group::Print machine info"
uname -a
if [ $(uname -s) == Darwin ]; then
sw_vers
# Print RAM in GB
RAM_BYTES=$(sysctl -n hw.memsize)
RAM_GB=$(echo "scale=2; $RAM_BYTES/1024/1024/1024" | bc)
echo "Available RAM (GB): $RAM_GB"
sysctl machdep.cpu.brand_string
sysctl machdep.cpu.core_count
# Print number of GPU cores (Apple Silicon)
if command -v system_profiler &> /dev/null; then
GPU_CORES=$(system_profiler SPDisplaysDataType | awk '/Total Number of Cores/ {print $5; exit}')
if [ -z "$GPU_CORES" ]; then
# Fallback: try to parse "Core Count" from Apple GPU section
GPU_CORES=$(system_profiler SPDisplaysDataType | awk '/Core Count/ {print $3; exit}')
fi
echo "GPU Cores: ${GPU_CORES:-Unknown}"
else
echo "system_profiler not available, cannot determine GPU cores."
fi
fi
echo "::endgroup::"
VR_MODE=""
if [ "${{ matrix.model.name }}" = "Voxtral-Mini-4B-Realtime-2602" ]; then
VR_MODE="vr-offline"
fi
${CONDA_RUN} bash .ci/scripts/test_model_e2e.sh metal "${{ matrix.model.repo }}/${{ matrix.model.name }}" "${{ matrix.quant }}" "${RUNNER_ARTIFACT_DIR}" "${VR_MODE}"