-
Notifications
You must be signed in to change notification settings - Fork 0
360 lines (325 loc) · 14.8 KB
/
Copy pathci.yml
File metadata and controls
360 lines (325 loc) · 14.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
name: CI
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
# A new push to the same ref supersedes the previous run.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Build-tool notes that apply to every job below:
#
# * Ninja, not Make. CMake's default generator on Linux/macOS is Unix
# Makefiles, and `cmake --build --parallel` with no job count passes a bare
# `-j` to make — which GNU make reads as *unlimited* parallelism. Enabling a
# GPU backend flips BROTENSOR_HAS_GPU in tests/CMakeLists.txt and pulls in
# the full GPU test tier (158 executables, up from 35), so an unbounded make
# forks ~160 compiles onto a 3-core / 7 GB hosted runner and the box spends
# its time swapping instead of compiling. Ninja bounds concurrency by
# default; the job counts below are pinned anyway.
#
# * ccache on the two expensive jobs (Metal: ~80 Objective-C++ TUs; CUDA: ~90
# .cu TUs). Cold runs are unaffected; a push that doesn't touch a backend
# gets those TUs back for free.
jobs:
# ── CPU tier ──────────────────────────────────────────────────────────────
#
# The CPU backend is always built and has no opt-out, so this is the tier
# every consumer gets. It is also the configuration most likely to rot
# unnoticed: day-to-day work happens in a CUDA-enabled build dir, where the
# 35 CPU-only tests are drowned out by the GPU suite and a CPU-only compile
# break never surfaces.
#
# macOS here is deliberately *without* Metal — it is the arm64 / AppleClang
# build check for the portable core (the SIMD baseline is x86-gated).
cpu:
name: CPU · ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { name: linux-gcc, os: ubuntu-24.04, cc: gcc, cxx: g++ }
- { name: linux-clang, os: ubuntu-24.04, cc: clang, cxx: clang++ }
- { name: windows-msvc, os: windows-2022 }
- { name: macos-arm64, os: macos-14 }
steps:
- uses: actions/checkout@v7
- name: Install Ninja (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ninja-build
- name: Install Ninja (macOS)
if: runner.os == 'macOS'
run: brew install ninja
# Windows keeps the Visual Studio generator: MSBuild's /m is already
# bounded, and the CPU tier is only 35 test targets there.
- name: Configure
env:
CC: ${{ matrix.cc }}
CXX: ${{ matrix.cxx }}
run: |
if [ "$RUNNER_OS" = "Windows" ]; then
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
else
cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
fi
shell: bash
- name: Build
run: cmake --build build --config Release --parallel 4
- name: Test
run: ctest --test-dir build -C Release --output-on-failure
# ── Metal ─────────────────────────────────────────────────────────────────
#
# Builds the ~80 .mm backend TUs. This is the only Apple toolchain in the
# loop — no development machine here runs one, so without this job a Metal
# change lands on inspection alone.
#
# ctest runs too: the GPU suite guards on is_available() and skips cleanly
# when the runner exposes no Metal device, so this is a build check at
# minimum and a real parity run when the runner has a GPU. Either way a
# genuine Metal parity failure fails the job rather than being silently
# skipped.
#
# Runner must be macos-15, not macos-14: src/metal/runtime.mm builds an
# offset-backed MPSGraphTensorData via -[MPSNDArray initWithBuffer:offset:
# descriptor:], which is a macOS 15 API. Against the macOS 14 SDK that
# selector is not declared and the TU does not compile. The CPU job above
# stays on macos-14 (it never touches MPS), so the older OS keeps its arm64
# coverage for the portable core.
#
# This is the heaviest job: the Metal backend *and* the full 158-executable
# test tier, on a small runner (3 vCPU / 7 GB). Hence Ninja + a pinned job
# count + ccache.
metal:
name: Metal · macos-15
runs-on: macos-15
steps:
- uses: actions/checkout@v7
- name: Install Ninja
run: brew install ninja
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: metal-macos-15
max-size: 500M
- name: Configure
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBROTENSOR_WITH_METAL=ON \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_OBJCXX_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build build --parallel 3
- name: Test
run: ctest --test-dir build --output-on-failure
# ── CUDA ──────────────────────────────────────────────────────────────────
#
# Compile-only: GitHub's hosted runners have no NVIDIA GPU, so nothing can be
# executed here. That still buys the thing that actually breaks — nvcc
# accepting all ~90 .cu TUs, and the device link (CUDA_RESOLVE_DEVICE_SYMBOLS)
# resolving. The parity *runs* stay on the local box / a self-hosted runner.
#
# CMAKE_CUDA_ARCHITECTURES must be pinned: the project defaults to `native`,
# which cannot be probed without a GPU present.
cuda:
name: CUDA build · ubuntu-24.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- name: Install Ninja
run: sudo apt-get update -qq && sudo apt-get install -y --no-install-recommends ninja-build
- name: Install CUDA toolkit (nvcc + cudart)
run: |
wget -q https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends cuda-nvcc-12-6 cuda-cudart-dev-12-6
echo "/usr/local/cuda-12.6/bin" >> "$GITHUB_PATH"
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: cuda-ubuntu-24.04
max-size: 500M
- name: Configure
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBROTENSOR_WITH_CUDA=ON \
-DCMAKE_CUDA_ARCHITECTURES=89 \
-DCMAKE_CXX_COMPILER_LAUNCHER=ccache \
-DCMAKE_CUDA_COMPILER_LAUNCHER=ccache
- name: Build
run: cmake --build build --parallel 4
# ── Coverage ──────────────────────────────────────────────────────────────
#
# Runs on the macOS runner *with Metal on*, not on a GPU-less Linux box, and
# that choice is the whole point of this job.
#
# Roughly 110 of the 158 tests are the CPU<->GPU parity suite, and every one
# of them computes its reference by calling the op on the CPU backend:
#
# brotensor::foo(X, cpu_Y, ...); // <- CPU backend, CPU path
# brotensor::foo(X.to(gpu), gpu_Y, ...); // <- GPU backend
# compare_tensors(cpu_Y, gpu_Y);
#
# So the parity suite is the single biggest exerciser of src/cpu/ — and it is
# gated behind BROTENSOR_HAS_GPU, meaning it does not even *build* without a
# GPU backend, let alone run. Measuring coverage on a GPU-less runner
# therefore reports the CPU backend with two-thirds of the tests that exercise
# it structurally excluded, understating it by tens of points. The hosted
# macos-15 runner has a real Metal device, so here the full suite runs and the
# number reflects what the tests actually reach.
#
# What is measured: the core (dispatcher, Tensor, safetensors/GGUF readers)
# plus the FP32 CPU reference backend. BROTENSOR_COVERAGE scopes --coverage to
# COMPILE_LANGUAGE:CXX, so src/metal's Objective-C++ TUs are not instrumented;
# they are excluded from the report rather than counted as 0%, which would
# make the headline number meaningless.
#
# No ccache: it interacts badly with --coverage (the .gcno sidecars are
# compile-time artifacts, and a cache hit skips emitting them).
#
# This job reports; it does not gate. gcovr's --fail-under-line would turn a
# coverage dip into a red build, which is a policy choice worth making only
# against a threshold the suite has held for a while.
coverage:
name: Coverage · CPU (via Metal parity suite)
runs-on: macos-15
steps:
- uses: actions/checkout@v7
- name: Install gcovr + Ninja
run: brew install gcovr ninja
- name: Configure (instrumented)
run: |
cmake -S . -B build -G Ninja \
-DCMAKE_BUILD_TYPE=Debug \
-DBROTENSOR_COVERAGE=ON \
-DBROTENSOR_WITH_METAL=ON
- name: Build
run: cmake --build build --parallel 3
- name: Test
run: ctest --test-dir build --output-on-failure
# pipefail matters here: piping gcovr into tee otherwise hands the step
# tee's exit status, so a gcovr crash produces no report and the job still
# goes green — a coverage step that silently reports nothing is worse than
# no coverage step at all.
#
# --gcov-executable: AppleClang emits gcov-format .gcno/.gcda, but the
# `gcov` on PATH is a shim that cannot read them. `llvm-cov gcov` is the
# matching reader.
#
# --gcov-ignore-parse-errors: gcov (both GCC's and llvm-cov's) can emit
# negative branch hit counts — a long-standing bug, gcc bugzilla 68080 —
# which gcovr treats as fatal. Downgrade to a per-file warning: line counts
# are unaffected, only a few branch counts in the offending files are.
- name: Report
shell: bash
run: |
set -o pipefail
mkdir -p coverage-html
gcovr --root . \
--gcov-executable 'xcrun llvm-cov gcov' \
--filter 'src/' --filter 'include/brotensor/' \
--exclude 'src/cuda/' --exclude 'src/metal/' \
--exclude 'tests/' \
--exclude-unreachable-branches \
--gcov-ignore-parse-errors negative_hits.warn_once_per_file \
--print-summary \
--cobertura coverage.xml --cobertura-pretty \
--html-details coverage-html/index.html \
| tee coverage-summary.txt
# The report must actually exist — guard against a silent no-op.
test -s coverage.xml
- name: Job summary
if: always()
run: |
{
echo '## Coverage — core + CPU backend'
echo
echo 'Measured on the macOS runner with Metal enabled, so the CPU↔GPU'
echo 'parity suite runs — it is the biggest exerciser of `src/cpu/`, since'
echo 'every parity test calls the CPU op as its reference, and it does not'
echo 'build at all without a GPU backend.'
echo
echo 'Scope: `src/` + `include/brotensor/`. The CUDA and Metal backends are'
echo 'compiled by nvcc / the Apple toolchain, are not gcov-instrumented, and'
echo 'are excluded rather than counted as 0%.'
echo
echo '```'
cat coverage-summary.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# Coverage lives entirely inside GitHub: the headline numbers land in the
# run's job summary (above) and the full line-by-line drill-down is this
# artifact. There is no third-party coverage service in the loop, by
# choice — it would add an external account and an upload token to the
# critical path of a build, in exchange for a badge.
- name: Upload HTML report
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-html
path: coverage-html/
retention-days: 14
# ── Pages ─────────────────────────────────────────────────────────────────
#
# Publishes the docs and the coverage report to
# https://wlejon.github.io/brotensor/.
#
# The coverage report is the reason this job exists. gcovr's --html-details
# output is the only view of *which lines* the suite misses, and as a build
# artifact it expired after 14 days — so the one artifact worth keeping was
# the one guaranteed to be gone by the time anyone wanted it. Here it gets a
# stable URL, rebuilt on every push to main.
#
# It reuses the artifact the coverage job already produced rather than
# re-running an instrumented build: that build is a 4-minute macOS job, and
# doing it twice per push to publish the same bytes would be silly.
#
# The docs come along because a coverage report with no docs around it is a
# dead end, and because op-coverage.md is the table the sibling projects
# actually want to link into.
pages:
name: Publish docs + coverage
needs: coverage
# Deploy only from main. A PR's build is not the published site, and the
# deploy needs write scopes a fork PR must never get.
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
permissions:
pages: write # deploy
id-token: write # OIDC token deploy-pages exchanges for the upload
contents: read
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
# Pages accepts one deployment at a time. Queue rather than cancel: a
# cancelled deploy leaves the previous site up, which is fine, but a
# cancelled *upload* has been known to wedge the environment.
concurrency:
group: pages
cancel-in-progress: false
steps:
- uses: actions/checkout@v7
- uses: actions/setup-python@v7
with:
python-version: "3.x"
- name: Render markdown -> site/
run: |
python -m pip install --quiet markdown
python .github/scripts/build_site.py site
# Into site/coverage/, which is where build_site.py's nav link points.
- name: Add the coverage report
uses: actions/download-artifact@v8
with:
name: coverage-html
path: site/coverage
- uses: actions/configure-pages@v6
- uses: actions/upload-pages-artifact@v5
with:
path: site
- name: Deploy
id: deploy
uses: actions/deploy-pages@v5