Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,11 @@ jobs:
build-webgpu-bridge:
needs: [changes, checks]
if: needs.changes.outputs.native == 'true'
name: ${{ matrix.upstream == 'pinned' && 'Build WebGPU Bridge (WASM)' || 'Build WebGPU Bridge (WASM v0.7.0)' }}
name: ${{ matrix.upstream == 'pinned' && 'Build WebGPU Bridge (WASM)' || 'Build WebGPU Bridge (WASM v0.9.0)' }}
strategy:
fail-fast: false
matrix:
upstream: [pinned, v0.7.0]
upstream: [pinned, v0.9.0]
runs-on: ubuntu-latest
env:
LLAMA_WEBGPU_SMOKE_MODEL_URL: https://huggingface.co/aladar/llama-2-tiny-random-GGUF/resolve/main/llama-2-tiny-random.gguf
Expand Down Expand Up @@ -111,8 +111,8 @@ jobs:
echo "error: llama_cpp.version is empty"
exit 1
fi
if [ "${{ matrix.upstream }}" = "v0.7.0" ]; then
LLAMA_CPP_TAG=v0.7.0
if [ "${{ matrix.upstream }}" = "v0.9.0" ]; then
LLAMA_CPP_TAG=v0.9.0
fi
echo "LLAMA_CPP_TAG=$LLAMA_CPP_TAG" >> "$GITHUB_ENV"
echo "Using llama.cpp ${LLAMA_CPP_TAG}"
Expand All @@ -138,8 +138,8 @@ jobs:
- name: Clone llama.cpp source
run: |
git clone --depth 1 --branch "$LLAMA_CPP_TAG" https://github.com/ggml-org/llama.cpp.git third_party/llama_cpp
if [ "${{ matrix.upstream }}" = "v0.7.0" ]; then
test "$(git -C third_party/llama_cpp rev-parse HEAD)" = 620d2225cc436ab0b001e3200e590e3b286ef9ab
if [ "${{ matrix.upstream }}" = "v0.9.0" ]; then
test "$(git -C third_party/llama_cpp rev-parse HEAD)" = cd959bae99ff4bed22bef2604f45241c09bbb8c8
fi

# Installed at a fixed path, not with setup-emsdk: its random temp
Expand Down Expand Up @@ -264,46 +264,46 @@ jobs:
if: failure()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.upstream == 'pinned' && 'state-persistence-smoke-artifacts' || 'state-persistence-smoke-artifacts-v0.7.0' }}
name: ${{ matrix.upstream == 'pinned' && 'state-persistence-smoke-artifacts' || 'state-persistence-smoke-artifacts-v0.9.0' }}
path: ${{ env.LLAMA_WEBGPU_SMOKE_ARTIFACTS_DIR }}
if-no-files-found: ignore

- name: Upload multimodal smoke diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.upstream == 'pinned' && 'multimodal-smoke-artifacts' || 'multimodal-smoke-artifacts-v0.7.0' }}
name: ${{ matrix.upstream == 'pinned' && 'multimodal-smoke-artifacts' || 'multimodal-smoke-artifacts-v0.9.0' }}
path: ${{ env.LLAMA_WEBGPU_MULTIMODAL_ARTIFACTS_DIR }}
if-no-files-found: ignore

- name: Upload grammar smoke diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.upstream == 'pinned' && 'grammar-smoke-artifacts' || 'grammar-smoke-artifacts-v0.7.0' }}
name: ${{ matrix.upstream == 'pinned' && 'grammar-smoke-artifacts' || 'grammar-smoke-artifacts-v0.9.0' }}
path: ${{ env.LLAMA_WEBGPU_GRAMMAR_ARTIFACTS_DIR }}
if-no-files-found: ignore

- name: Upload next-token scores smoke diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.upstream == 'pinned' && 'next-token-scores-smoke-artifacts' || 'next-token-scores-smoke-artifacts-v0.7.0' }}
name: ${{ matrix.upstream == 'pinned' && 'next-token-scores-smoke-artifacts' || 'next-token-scores-smoke-artifacts-v0.9.0' }}
path: ${{ env.LLAMA_WEBGPU_NEXT_TOKEN_SCORES_ARTIFACTS_DIR }}
if-no-files-found: ignore

- name: Upload speculative decoding smoke diagnostics
if: failure()
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.upstream == 'pinned' && 'speculative-smoke-artifacts' || 'speculative-smoke-artifacts-v0.7.0' }}
name: ${{ matrix.upstream == 'pinned' && 'speculative-smoke-artifacts' || 'speculative-smoke-artifacts-v0.9.0' }}
path: ${{ env.LLAMA_WEBGPU_SPECULATIVE_ARTIFACTS_DIR }}
if-no-files-found: ignore

- name: Upload bridge artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.upstream == 'pinned' && 'webgpu-bridge-dist' || 'webgpu-bridge-dist-v0.7.0' }}
name: ${{ matrix.upstream == 'pinned' && 'webgpu-bridge-dist' || 'webgpu-bridge-dist-v0.9.0' }}
path: |
${{ runner.temp }}/webgpu_bridge_dist/llama_webgpu_bridge.js
${{ runner.temp }}/webgpu_bridge_dist/llama_webgpu_bridge_worker.js
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ GGUFs or smoke artifacts.

- CI build gate: `.github/workflows/ci.yml`
- Builds wasm32/memory64 against the `llama_cpp.version` pin and exact
v0.7.0 candidate in separate lanes. Its exact SHA lives in the workflow.
v0.9.0 candidate in separate lanes. Its exact SHA lives in the workflow.
Preserve its check/artifact identities (`Build WebGPU Bridge (WASM)`,
`webgpu-bridge-dist`). Neither lane publishes or changes the source pin; candidate check
and artifact identities use the `v0.7.0` suffix. See
`docs/v070_qualification.md` for the preparation evidence and limits.
and artifact identities use the `v0.9.0` suffix. See
`docs/v090_qualification.md` for the preparation evidence and limits.
Builds against llama.cpp v0.4.0 are no longer tested in CI; the
`src/llama_webgpu_mtmd_compat.h` shim and its static contract remain.
- Verifies the active `emcc` identity against `emsdk.version` and
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,16 +215,16 @@ This repo includes a wasm build gate in:
- `.github/workflows/ci.yml`

After the JS/compatibility contracts pass, it builds wasm32 and memory64 against
the pinned `llama.cpp` tag in `llama_cpp.version` and the exact v0.7.0
the pinned `llama.cpp` tag in `llama_cpp.version` and the exact v0.9.0
candidate in separate lanes. Both run real
state-persistence, multimodal, grammar and next-token-score browser smokes. A
successful run uploads its seven built files as `webgpu-bridge-dist`; a failed
run uploads no dist and instead the smoke diagnostics that exist
(`state-persistence-smoke-artifacts`, `multimodal-smoke-artifacts`,
`grammar-smoke-artifacts`, `next-token-scores-smoke-artifacts`). Candidate checks and artifacts use the `v0.7.0` suffix;
`grammar-smoke-artifacts`, `next-token-scores-smoke-artifacts`). Candidate checks and artifacts use the `v0.9.0` suffix;
the pinned identities remain unchanged. CI never changes a pin or publishes
assets. Builds against llama.cpp v0.4.0 are no longer tested in CI. See
[the v0.7.0 preparation evidence](docs/v070_qualification.md) for validation
[the v0.9.0 preparation evidence](docs/v090_qualification.md) for validation
and qualification limits.
To run the media-helper and static CI contracts locally:

Expand Down
46 changes: 46 additions & 0 deletions docs/v090_qualification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# llama.cpp v0.9.0 compatibility preparation

The candidate lane builds upstream `cd959bae99ff4bed22bef2604f45241c09bbb8c8`
(v0.9.0) alongside the unchanged v0.5.0 development pin, matching published
native `v0.9.0`. The bridge source needs no change for v0.9.0. No Web assets or
Dart packages are published by this change.

## Upstream and native patches

- The v0.7.0 embedding-mode reserve repair
([llamadart-native#114](https://github.com/leehack/llamadart-native/issues/114),
[v0.7.0 notes](v070_qualification.md)) is upstream since v0.8.0:
`set_embeddings` re-reserves on every toggle. `cmake/embedding_mode_reserve.cmake`
finds no affected guard and stays inactive.
- The other native patches do not apply to the Web build: 0001/0002 are
Vulkan, 0004 Metal, and 0005/0006 (F16 dot and GEMM F32 accumulation,
[llamadart-native#174](https://github.com/leehack/llamadart-native/issues/174))
change code only under `__ARM_NEON`/`__ARM_FEATURE_FP16_VECTOR_ARITHMETIC`,
which WASM never defines.
- The mtmd-audio single-thread rewrite still finds both `n_threads` markers.
- v0.9.0 drops `mtmd_helper_gen_audio_inp::voice` and adds
`mtmd_gen_audio_info::n_voices` and Chatterbox voices. The bridge TTS path
sets neither field, so Qwen3-TTS, with or without a speaker reference, is
unaffected.

## Validation

Local evidence uses Emscripten 6.0.8, Node 24 and the locked Playwright
Chromium on macOS arm64, against a clean upstream checkout at the commit above.
Model/projector pins and commands live in
[CONTRIBUTING.md](../CONTRIBUTING.md#validate-outputs).

| Check | Result |
| --- | --- |
| `npm run check:js` | 602 passed, 4 existing skips, zero failures |
| CI reliability contract | PASS |
| v0.9.0 production build | PASS, wasm32 and memory64 |
| Extraction/state/generation smoke | PASS, direct and worker on both memory modes |
| Qwen3.5 image inference | PASS, direct and worker on wasm32 |
| Tiny and Qwen3.5 grammar, next-token scores, tiny speculative | PASS |
| Qwen3-ASR transcript, silence and cancellation | PASS, four runtime rows |
| Qwen3-TTS synthesis, cancellation and unload, WebGPU offload | PASS, four runtime rows with a speaker reference; direct and worker on memory64 without one |

Four runtime rows mean direct and worker execution on wasm32 and memory64.
Hosted exact-head CI, candidate qualification, publication approval and Dart
adoption remain separate gates.
11 changes: 6 additions & 5 deletions tests/ci/ci_scope_test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,8 @@ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../.
const { jobs } = workflow;
const build = jobs['build-webgpu-bridge'];
const { checks } = jobs;
assert.equal(build.name, "${{ matrix.upstream == 'pinned' && 'Build WebGPU Bridge (WASM)' || 'Build WebGPU Bridge (WASM v0.7.0)' }}");
assert.deepEqual(build.strategy.matrix.upstream, ['pinned', 'v0.7.0']);
assert.equal(build.name, "${{ matrix.upstream == 'pinned' && 'Build WebGPU Bridge (WASM)' || 'Build WebGPU Bridge (WASM v0.9.0)' }}");
assert.deepEqual(build.strategy.matrix.upstream, ['pinned', 'v0.9.0']);
assert.equal(build.strategy['fail-fast'], false);
assert.deepEqual(build.needs, ['changes', 'checks']);
assert.equal(build.if, "needs.changes.outputs.native == 'true'");
Expand All @@ -170,15 +170,16 @@ const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../.
1,
);
const clone = build.steps.find((step) => step.name === 'Clone llama.cpp source').run;
assert.match(clone, /620d2225cc436ab0b001e3200e590e3b286ef9ab/);
assert.match(clone, /cd959bae99ff4bed22bef2604f45241c09bbb8c8/);
assert.match(clone, /"\$\{\{ matrix.upstream \}\}" = "v0.9.0"/);
assert.match(clone, /git -C third_party\/llama_cpp rev-parse HEAD/);
const resolver = build.steps.find((step) => step.name === 'Resolve llama.cpp pin').run;
assert.match(resolver, /llama_cpp.version/);
assert.match(resolver, /matrix.upstream/);
assert.match(resolver, /LLAMA_CPP_TAG=v0.7.0/);
assert.match(resolver, /LLAMA_CPP_TAG=v0.9.0/);
for (const step of build.steps.filter((step) => step.uses?.startsWith('actions/upload-artifact@'))) {
assert.match(step.with.name, /matrix.upstream == 'pinned'/);
assert.match(step.with.name, /-v0.7.0'/);
assert.match(step.with.name, /-v0.9.0'/);
}
const buildSteps = Object.fromEntries(build.steps.map((step) => [step.name, step]));
for (const name of ['Run state persistence browser smoke', 'Run multimodal browser smoke', 'Build bridge artifacts', 'Verify outputs']) {
Expand Down
Loading