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
97 changes: 97 additions & 0 deletions .github/workflows/bump-zinc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright 2026 bong-water-water-bong
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Keep third_party/zinc on upstream zolotukhin/zinc main (docs/zinc.md): when
# upstream moves, open a PR here moving the submodule. GitHub-hosted CI has no
# GPU; rerun scripts/build-zinc.sh and the smoke test in docs/zinc.md on Strix
# Halo before merging.
#
# Uses the secret HRX_BUMP_TOKEN (Contents and Pull requests read/write on
# 1bit-MONSTER/engine): a PR opened with the default GITHUB_TOKEN would not
# start CI.
name: bump-zinc

on:
schedule:
- cron: "53 6 * * *"
workflow_dispatch:

permissions:
contents: read

concurrency:
group: bump-zinc
cancel-in-progress: false

jobs:
bump:
runs-on: ubuntu-latest
steps:
- name: Require the token
env:
HRX_BUMP_TOKEN: ${{ secrets.HRX_BUMP_TOKEN }}
run: |
if [ -z "$HRX_BUMP_TOKEN" ]; then
echo "::error::secret HRX_BUMP_TOKEN is not set (see the header of this workflow)"
exit 1
fi

- uses: actions/checkout@v4
with:
token: ${{ secrets.HRX_BUMP_TOKEN }}

- name: Compare with upstream
id: pins
env:
GH_TOKEN: ${{ secrets.HRX_BUMP_TOKEN }}
run: |
set -euo pipefail
upstream=$(gh api repos/zolotukhin/zinc/commits/main --jq .sha)
ours=$(git ls-tree HEAD third_party/zinc | awk '{print $3}')
echo "upstream $upstream, ours $ours"
{
echo "upstream=$upstream"; echo "ours=$ours"
if [ "$upstream" = "$ours" ]; then echo "changed=false"; else echo "changed=true"; fi
} >> "$GITHUB_OUTPUT"

- name: Open the bump PR
if: steps.pins.outputs.changed == 'true'
env:
GH_TOKEN: ${{ secrets.HRX_BUMP_TOKEN }}
UPSTREAM: ${{ steps.pins.outputs.upstream }}
OURS: ${{ steps.pins.outputs.ours }}
run: |
set -euo pipefail
branch="bump-zinc/${UPSTREAM:0:12}"
if git ls-remote --exit-code origin "refs/heads/$branch" > /dev/null; then
echo "$branch already exists"; exit 0
fi
log=$(gh api "repos/zolotukhin/zinc/compare/${OURS}...${UPSTREAM}" \
--jq '.commits[-30:][] | "- \(.sha[0:9]) \(.commit.message | split("\n")[0])"' || true)
git config user.name "zinc-bump"
git config user.email "zinc-bump@users.noreply.github.com"
git switch -c "$branch"
git update-index --cacheinfo "160000,$UPSTREAM,third_party/zinc"
git commit -q -m "Bump ZINC: zolotukhin/zinc ${UPSTREAM:0:12}"
git push -q origin "$branch"
gh pr create --base main --head "$branch" \
--title "Bump ZINC: zolotukhin/zinc ${UPSTREAM:0:12}" \
--body "Moves third_party/zinc from \`${OURS:0:12}\` to upstream main \`${UPSTREAM:0:12}\`.

Upstream commits (last 30):
${log}

CI here has no GPU. Before merging, on Strix Halo:
\`scripts/build-zinc.sh ~/.cache/zinc-pin vulkan && RADV_PERFTEST=coop_matrix ~/.cache/zinc-pin/vulkan/bin/zinc -m ~/models/Qwen3-0.6B-Q4_K_M.gguf --prompt 'The capital of France is'\` (first token 12095)."
7 changes: 7 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@
path = third_party/xdna-driver
url = https://github.com/amd/xdna-driver.git
branch = main
# ZINC (docs/zinc.md): upstream zolotukhin/zinc, built by scripts/build-zinc.sh;
# .github/workflows/bump-zinc.yml keeps it current.
[submodule "third_party/zinc"]
path = third_party/zinc
url = https://github.com/zolotukhin/zinc.git
branch = main
shallow = true
22 changes: 22 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,28 @@ if(ONEBIT_HRX)
COMMAND ${CMAKE_SOURCE_DIR}/tests/hrx_lemonade_e2e.sh $<TARGET_FILE:onebit> ${ONEBIT_HRX_SERVER})
endif()

# ── ZINC (docs/zinc.md) ────────────────────────────────────────────────────
# Off by default: it builds third_party/zinc with scripts/build-zinc.sh (which
# fetches its own Zig) for one GPU backend. `1bit lemonade` then serves the zinc
# recipe with this binary. ONEBIT_ZINC_BACKEND=cuda is the NVIDIA build.
option(ONEBIT_ZINC "Build ZINC (third_party/zinc) and wire it into Lemonade" OFF)
if(ONEBIT_ZINC)
set(ONEBIT_ZINC_BACKEND "vulkan" CACHE STRING "ZINC GPU backend: vulkan, rocm or cuda")
set_property(CACHE ONEBIT_ZINC_BACKEND PROPERTY STRINGS vulkan rocm cuda)
if(NOT EXISTS "${CMAKE_SOURCE_DIR}/third_party/zinc/build.zig")
message(FATAL_ERROR "ONEBIT_ZINC needs third_party/zinc: git submodule update --init third_party/zinc")
endif()
set(ONEBIT_ZINC_SERVER "${CMAKE_BINARY_DIR}/zinc/${ONEBIT_ZINC_BACKEND}/bin/zinc")
add_custom_target(zinc
COMMAND ${CMAKE_SOURCE_DIR}/scripts/build-zinc.sh ${CMAKE_BINARY_DIR}/zinc ${ONEBIT_ZINC_BACKEND}
BYPRODUCTS ${ONEBIT_ZINC_SERVER}
USES_TERMINAL)
add_dependencies(onebit zinc)
target_compile_definitions(onebit PRIVATE ONEBIT_ZINC_SERVER="${ONEBIT_ZINC_SERVER}")
add_test(NAME zinc_lemonade_e2e
COMMAND ${CMAKE_SOURCE_DIR}/tests/zinc_lemonade_e2e.sh $<TARGET_FILE:onebit> ${ONEBIT_ZINC_SERVER})
endif()

# ── Step 3: NPU engine, full ELFs only (docs/npu.md) ──────────────────────
# Host side (full-ELF generation, model reader, buffer packing): pure C++, no
# XRT. It needs PCRE2 (the tokenizer), and macOS has no NPU, so it is off there
Expand Down
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@ with AMD Ryzen AI hardware behind it:

- the XDNA 2 NPU engine
- HRX and Vulkan on the Radeon iGPU, compiled together in one llama.cpp build
- ZINC, which also reaches NVIDIA GPUs (CUDA) and Apple GPUs (Metal)
- Laya, which decides where each request runs
- every Hugging Face model architecture, kept current by a daily census

> **Status:** steps 1–2 of 5 have landed: embedded Lemonade ([docs/lemonade.md](docs/lemonade.md))
> and HRX + Vulkan in one build ([docs/hrx.md](docs/hrx.md)). The working engine is being ported from
> [1bit-MONSTER](https://github.com/1bit-MONSTER/1bit-MONSTER) in five steps; see
> [docs/PORTING.md](docs/PORTING.md). This repository holds the verified code
> **Status:** steps 1–3 of 5 have landed: embedded Lemonade ([docs/lemonade.md](docs/lemonade.md)),
> HRX + Vulkan in one build ([docs/hrx.md](docs/hrx.md)), and the NPU fast lane on full ELFs
> ([docs/npu.md](docs/npu.md)). Also: MLX on Apple Silicon ([docs/apple.md](docs/apple.md)), and
> ZINC with NVIDIA through its CUDA backend ([docs/zinc.md](docs/zinc.md)). The working engine is
> being ported from [1bit-MONSTER](https://github.com/1bit-MONSTER/1bit-MONSTER); see
> [docs/PORTING.md](docs/PORTING.md). Measured results are on the
> [wiki](https://github.com/1bit-MONSTER/engine/wiki). This repository holds the verified code
> without the development history.

## License
Expand Down
13 changes: 13 additions & 0 deletions app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ void use_engine_hrx_build(nlohmann::json& config) {
}
#endif

#ifdef ONEBIT_ZINC_SERVER
// Points Lemonade's zinc recipe at this build's zinc (third_party/zinc,
// docs/zinc.md) unless the user already chose a binary.
void use_engine_zinc_build(nlohmann::json& config) {
auto& sec = config["zinc"];
if (!sec.is_object()) sec = nlohmann::json::object();
if (!sec.contains("zinc_bin") || sec["zinc_bin"] == "") sec["zinc_bin"] = ONEBIT_ZINC_SERVER;
}
#endif

// Native models for the `onebit` backend: every NPU model directory
// (docs/npu.md) directly under the roots in ONEBIT_MODEL_ROOTS (colon-separated,
// default ~/models). Lemonade serves each through `1bit unified -m <dir>`. The
Expand Down Expand Up @@ -142,6 +152,9 @@ int run_lemonade(int argc, char** argv) {
#ifdef ONEBIT_HRX_SERVER
use_engine_hrx_build(config_json);
#endif
#ifdef ONEBIT_ZINC_SERVER
use_engine_zinc_build(config_json);
#endif

lemon::backends::onebit::set_onebit_models(native_models());

Expand Down
1 change: 1 addition & 0 deletions docs/PORTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Each step below is one PR (or a short series) that builds and runs on Strix Halo
| 3 | **NPU engine.** Full ELFs only, and the open 16-tile layer kernel built from source | `engine/npu` (`npu_engine_universal.cpp`, `I8Ctx::init_elf`); ELF dispatch table on `backup/iso-build-elf-native-2026-09-22`; kernel on `bench/fastlane-16tile-corrections-2026-09-22` | **3a landed** ([docs/npu.md](npu.md)): full ELFs generated in C++, reproducing all 5,153 captured contexts. **3b/3c**: the lane runtime (logits bit-identical to the reference lane, 24/24 steps, 11.0 ms/token), tokenizer, `1bit unified`, NPU models registered with `onebit`. Done when Lemonade loads Qwen3-0.6B on the NPU through `onebit` |
| 4 | **Laya router.** A non-autoregressive scorer that picks where each request runs | `src/laya_scorer.cpp`, `include/laya_scorer.h` on `backup/laya-and-results-2026-09-22`; model at `~/models/laya` | matches its Python reference; routes requests |
| 5 | **Every HF model, kept current.** The architecture registry (569 tokens mapping 2,030 HF arch strings) and the daily HF census that finds new architectures and proposes mappings | `src/model_registry*.cpp`, `Testing/census_*.py` and `.json`, `.github/workflows/census-{watch,sweep,autopr}.yml` | the census runs daily in CI; docs report *mapped* and *run and checked* counts separately |
| 6 | **ZINC (NVIDIA and more).** Upstream `zolotukhin/zinc`, a Zig GGUF engine with Vulkan, ROCm, CUDA and Metal backends; its CUDA backend reaches NVIDIA GPUs (Ada `sm_89`, Blackwell `sm_120`) | not in 1bit-MONSTER; pinned from upstream `main` | **pinned** ([docs/zinc.md](zinc.md)): `scripts/build-zinc.sh` builds it privately; the Vulkan build gives 12095 (" Paris") at 295 tok/s on Strix Halo; the CUDA build answers " Paris." at 167–173 tok/s on an RTX 5090 (Qwen3.5-9B); kept current by `bump-zinc.yml`. `1bit lemonade` serves it as the `zinc` recipe (`-DONEBIT_ZINC=ON`, e2e passes) |

## How the pieces fit

Expand Down
100 changes: 100 additions & 0 deletions docs/zinc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<!--
Copyright 2026 bong-water-water-bong
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# ZINC

[ZINC](https://github.com/zolotukhin/zinc) (MIT) is a single-binary GGUF engine
written in Zig. It has its own kernels for four GPU backends, and serves
`/health`, `/v1/models` and `/v1/chat/completions`:

| Backend | Hardware | Build needs |
|---|---|---|
| `vulkan` | AMD RDNA (Strix Halo gfx1151), any Vulkan GPU | `glslc`, the Vulkan loader |
| `rocm` | AMD, HIP | ROCm in `ROCM_PATH` (default `/opt/rocm`) |
| `cuda` | NVIDIA Ada `sm_89`, Blackwell `sm_120` | CUDA toolkit in `CUDA_HOME` (default `/usr/local/cuda`) |
| `metal` | Apple Silicon | Xcode command-line tools |

The CUDA backend is how the engine reaches NVIDIA GPUs. It is a C shim over the
driver API, NVRTC and cuBLAS (`src/cuda/cuda_shim.c`). Its int8 GEMMs use
`__dp4a`, the CUDA counterpart of AMD's `v_dot4_i32_i8`. Upstream's design notes
are in `third_party/zinc/docs/cuda-backend.md`.

## The pin

`third_party/zinc` is upstream `zolotukhin/zinc` `main` at a fixed commit. ZINC
changes daily, so the engine builds only the commit it pins, and
`.github/workflows/bump-zinc.yml` moves that pin forward (see "How it stays
current").

## Build

```sh
git submodule update --init third_party/zinc
scripts/build-zinc.sh ~/.cache/zinc-pin vulkan # or rocm, cuda
~/.cache/zinc-pin/vulkan/bin/zinc -m model.gguf --prompt "The capital of France is"
```

The script needs nothing from the system except the backend's own toolkit:

- **Zig.** It downloads the Zig release that ZINC's `build.zig.zon` names as
`minimum_zig_version` (currently 0.15.2) into `<prefix>/zig-<version>`. The
download is checked against the sha256 that ziglang.org publishes.
- **Build output.** Zig's caches go under `<prefix>`, so the submodule stays
clean. The binary is `<prefix>/<backend>/bin/zinc`.

## Verified (pin `c50b4add`, 2026-09-23)

| Backend | Result |
|---|---|
| `vulkan` | builds. Qwen3-0.6B Q4_K_M, prompt `785,6722,315,9625,374`: first token **12095** (" Paris"), the same as the NPU lane and the CPU reference. Prefill 817 tok/s; decode **295 tok/s** (3.4 ms/tok) with `RADV_PERFTEST=coop_matrix`, which ZINC expects on RADV. |
| `rocm` | builds and links the system ROCm 7 (`libamdhip64.so.7`, `libhsa-runtime64`). Not run on a model: ZINC's ROCm forward pass implements only the Qwen3.5/3.6 family, and it rejects `qwen3` GGUFs with `UnsupportedArchitecture`. |
| `cuda` | **RTX 5090** (`sm_120`, rented on Clore.ai; driver 615.71, CUDA 13.2 from NVIDIA's apt repo, `CUDA_HOME=/usr/local/cuda-13.2`). `scripts/build-zinc.sh <prefix> cuda` builds. With `qwen35-9b-q4k-m` (Qwen3.5-9B Q4_K_M, ZINC's catalog) the continuation of "The capital of France is" is **" Paris."**. Decode 167–173 tok/s (5.8–6.0 ms/tok); prefill 309 tok/s on the 5-token prompt. |

ZINC's CUDA and ROCm forward passes share one implementation. It covers only
the Qwen3.5/3.6 family: `qwen3` GGUFs fail with `UnsupportedArchitecture`, and
Qwen3.5-0.8B (`UD-Q4_K_XL`) fails with `MissingTensor`. Vulkan runs `qwen3`.

ZINC has no BF16 weight path, so use a quantized GGUF. For example, the
BF16 golden `Qwen3-0.6B-BF16.gguf` fails with `UnsupportedQuantType` (type 30).

## Served by Lemonade

With `-DONEBIT_ZINC=ON` (and `-DONEBIT_ZINC_BACKEND=vulkan|rocm|cuda`, default
`vulkan`), the build runs `scripts/build-zinc.sh` into `build/zinc/`, and
`1bit lemonade` sets the `zinc` recipe's `zinc.zinc_bin` to that binary. A value
you set yourself wins. The recipe is the local Lemonade delta 7
(`third_party/lemonade/UPSTREAM.md`):

- **Models.** Checkpoints are GGUF files that Lemonade downloads and resolves
with llamacpp's own rules. `Qwen3-0.6B-ZINC` is
`unsloth/Qwen3-0.6B-GGUF:Q4_K_M`.
- **Load.** `zinc -m <gguf> -p <port> -c <ctx_size>`, with
`RADV_PERFTEST=coop_matrix` unless you already set it, then wait on `/health`.
- **Requests.** Sent without `model`, because zinc rejects any id except its
own. Replies carry the Lemonade name back.

`tests/zinc_lemonade_e2e.sh` (ctest `zinc_lemonade_e2e`) passes on Strix Halo:
pull, "Paris." under `Qwen3-0.6B-ZINC`, served by this build's zinc, and
streaming. Lemonade's `/stats` reports 0 tok/s for zinc, because zinc's replies
have no `timings` block.

## How it stays current

`.github/workflows/bump-zinc.yml` runs daily. When upstream `main` has moved, it
opens a PR that moves `third_party/zinc` and lists the new upstream commits. It
uses the same `HRX_BUMP_TOKEN` secret as `bump-hrx.yml` (see [hrx.md](hrx.md)).
GitHub-hosted CI has no GPU, so before merging a bump, rerun the build and the
smoke test above on Strix Halo.
67 changes: 67 additions & 0 deletions scripts/build-zinc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Copyright 2026 bong-water-water-bong
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# build-zinc.sh <prefix> [backend]
#
# Builds ZINC pinned in third_party/zinc (upstream zolotukhin/zinc) into
# <prefix>/<backend>/bin/zinc. backend is one of vulkan (default), rocm, cuda:
# vulkan needs glslc and the Vulkan loader
# rocm needs a ROCm/TheRock root in ROCM_PATH (default /opt/rocm)
# cuda needs the CUDA toolkit in CUDA_HOME (default /usr/local/cuda); an
# NVIDIA box (on WSL2, libcuda comes from /usr/lib/wsl/lib)
#
# Zig is the exact release zinc's build.zig.zon names as minimum_zig_version,
# downloaded once into <prefix>/zig-<version> and checked against the sha256
# ziglang.org publishes. Nothing is installed system-wide (docs/zinc.md).
set -euo pipefail
prefix=${1:?usage: build-zinc.sh <prefix> [vulkan|rocm|cuda]}
backend=${2:-vulkan}
case "$backend" in vulkan|rocm|cuda) ;; *) echo "unknown backend: $backend"; exit 1 ;; esac
root=$(cd "$(dirname "$0")/.." && pwd)
src=$root/third_party/zinc
mkdir -p "$prefix"
prefix=$(cd "$prefix" && pwd)

[ -f "$src/build.zig" ] || { echo "third_party/zinc is empty: git submodule update --init third_party/zinc"; exit 1; }

ver=$(sed -n 's/.*minimum_zig_version *= *"\([^"]*\)".*/\1/p' "$src/build.zig.zon")
[ -n "$ver" ] || { echo "no minimum_zig_version in third_party/zinc/build.zig.zon"; exit 1; }
arch=$(uname -m)
zig_dir=$prefix/zig-$ver
if [ ! -x "$zig_dir/zig" ]; then
key="$arch-linux"
read -r url sha < <(curl -fsSL https://ziglang.org/download/index.json |
python3 -c 'import json,sys; e=json.load(sys.stdin)[sys.argv[1]][sys.argv[2]]; print(e["tarball"], e["shasum"])' "$ver" "$key")
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
curl -fsSL "$url" -o "$tmp/zig.tar.xz"
echo "$sha $tmp/zig.tar.xz" | sha256sum -c --quiet
mkdir -p "$zig_dir"
tar -xJf "$tmp/zig.tar.xz" -C "$zig_dir" --strip-components=1
fi
"$zig_dir/zig" version

commit=$(git -C "$src" rev-parse --short=12 HEAD 2>/dev/null || echo unknown)
out=$prefix/$backend
# Keep zig's caches out of the source tree so the submodule stays clean.
(cd "$src" && "$zig_dir/zig" build -Dbackend="$backend" -Doptimize=ReleaseFast \
-Dcommit="$commit" \
--cache-dir "$prefix/.zig-cache/$backend" \
--global-cache-dir "$prefix/.zig-global-cache" \
--prefix "$out")

echo "ZINC ($backend, $commit): $out/bin/zinc"
"$out/bin/zinc" --version
Loading
Loading