-
Notifications
You must be signed in to change notification settings - Fork 954
288 lines (262 loc) · 11.6 KB
/
metal.yml
File metadata and controls
288 lines (262 loc) · 11.6 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
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
jobs:
test-metal-builds:
name: test-executorch-metal-build
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
with:
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
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
with:
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
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
with:
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)
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
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:
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")
${CONDA_RUN} bash .ci/scripts/export_model_artifact.sh metal "${{ matrix.model.repo }}/${{ matrix.model.name }}" "${{ matrix.quant }}" "${RUNNER_ARTIFACT_DIR}"
test-model-metal-e2e:
name: test-model-metal-e2e
needs: export-model-metal-artifact
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:
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::"
${CONDA_RUN} bash .ci/scripts/test_model_e2e.sh metal "${{ matrix.model.repo }}/${{ matrix.model.name }}" "${{ matrix.quant }}" "${RUNNER_ARTIFACT_DIR}"