41 KiB
Strata on an Intel Arc: the SYCL engine
This page is for people who build the Intel Arc engine from source or run it in a container, and for those who maintain the port. For the released Linux engine package, start with INTEL_ARC.md.
Strata's engine is CUDA. On an Intel Arc it runs as Strata's own engine ported to SYCL (sycl/), behind the same server:
OpenAI and Anthropic APIs, streaming, tool calls, MCP and the web app are unchanged. Everything Intel-specific lives in sycl/;
no shared upstream file is changed. sycl/setup_intel.py and sycl/serve/server_intel.py wrap upstream's setup.py and
serve/server.py from outside. llama.cpp's SYCL backend runs the same GGUF at about a third of the speed.
DeepSeek V4.1-Flash does not run on Intel. Pictures are on by default (below).
Quick start
On Ubuntu 24.04 or newer, with Intel's GPU driver installed (ls /dev/dri shows a renderD128; see
INTEL_ARC.md):
sudo apt install docker.io git
sudo usermod -aG docker,render $USER # log out and in; `docker ps` must work without sudo
cd Strata
docker build --pull -t strata-sycl-dev sycl/tools # once, about 6 minutes, 13 GB
# build the engine: see "How to build it", then
./setup.sh --backend sycl [setup's options, e.g. --model IQ3_S --context 32768 --port 8085]
setup.sh makes its own Python environment in .venv. Do not run python3 sycl/setup_intel.py yourself: Ubuntu's system
Python refuses it (externally-managed-environment). ./setup.sh --backend sycl is the one command for the first install and
every later start. Then run-<model>.sh starts the model; the first start of a 30 GB model takes about two minutes.
Setup
sycl/setup_intel.py imports upstream's setup.py and replaces the Intel steps. The model choice, download, pack, tokenizer,
MTP draft layer and the context and KV questions are setup's own.
| Part | What changes on an Arc |
|---|---|
| GPU check | The Arc is found in sysfs (vendor 8086 under xe or i915) and offered through setup's AMD path (it builds locally and has no images). |
| Engine | Uses the SYCL build (build-sycl-aot/strata or build-sycl/strata, run in the strata-sycl-dev image by sycl/serve/strata-sycl.sh). |
| RAM rule | Does not apply on an xe card: the port streams the experts from the GGUF into VRAM (--stream-experts), so RAM only decides KV streaming. --check lists what fits by VRAM. |
| Config | Container paths and "backend": "sycl". VRAM reserve 1,024 MiB up to 32K context and 2,048 MiB above, 300 MiB on a card under 12 GB. A --vram-reserve-mib N you give is kept. KV streaming (--kv-resident 32768) is on from 64K when the RAM holds the KV. |
| Fit rule | A model that does not fit the card at the default context gets a context down the ladder 32K, 16K, 8K, a smaller prompt chunk and a lower reserve until it does. Setup prints one plain line per change. |
| Prompt chunk | 4,096 tokens; 8,192 on a Battlemage card with 24 GB or more above 32K context. A --prefill in the config is kept. |
| Alchemist (A-series) | A different config: Arc A750 and the other Alchemist cards. |
| Engine settings | The config's "env" block reaches the engine. strata-sycl.sh forwards every STRATA_, ONEAPI_, UR_, IGC_, SYCL_ and ZES_ variable and sets STRATA_VERIFY_DEVICE_PLAN=1, STRATA_VERIFY_NO_HOST=1 and STRATA_STAGER_THREADS=12 unless the environment says otherwise (for example "env": {"STRATA_VERIFY_NO_HOST": "0"}). A value of 0 is not passed on, because the engine tests these switches by presence. |
The models, packs and checkout must sit under the folder strata-sycl.sh mounts at /work (the one above the checkout, or
STRATA_SYCL_ROOT). An earlier Strata install on the same PC keeps its data folder in ~/.config/strata/settings.json; pass
--data-dir <folder above the checkout>/Strata-data to put this one under the mount. A symlink to a folder outside the mount does
not work in the container; a hard link does. If a future setup.py drops a step this relies on, setup stops with a message
instead of writing a wrong config.
How to build it
The engine is built in Docker, in the strata-sycl-dev image (oneAPI 2026.1 compiler, oneMKL and Level Zero, from a community
llama.cpp SYCL image), so the PC needs no oneAPI. From the checkout (the folder above it is what the container mounts at
/work):
cd Strata
docker build --pull -t strata-sycl-dev sycl/tools # once: about 6 minutes, a 13 GB image
H=$(basename "$PWD")
Arc B-series (Pro B70: bmg-g31; B580, B570 and Pro B60: bmg-g21; ocloc ids bmg-g21 in the image lists device ids).
Ahead-of-time code, no compile at the first start:
docker run --rm -u $(id -u):$(id -g) -v "$PWD/..:/work" -e AOT=bmg-g31 -e REPO=/work/$H \
-e BUILD_DIR=/work/$H/build-sycl-aot strata-sycl-dev "cd /work/$H && bash sycl/tools/build.sh strata"
Arc A-series (Alchemist: A750, A770, A580, A380): no AOT=. The code is compiled from SPIR-V at the first start (JIT, about
a minute). Nobody has run an AOT build for these cards.
docker run --rm -u $(id -u):$(id -g) -v "$PWD/..:/work" -e REPO=/work/$H \
-e BUILD_DIR=/work/$H/build-sycl strata-sycl-dev "cd /work/$H && bash sycl/tools/build.sh strata"
The build takes about 3 minutes and ends with BUILD EXIT 0 and errors: 0. Setup finds build-sycl-aot/strata or
build-sycl/strata. Run the docker build again after pulling an update that changes sycl/tools/Dockerfile, and the
docker run after every engine update (setup says when the engine is older than it needs).
build.sh variable |
Effect |
|---|---|
AOT=bmg-g31 or bmg-g21 |
Ahead-of-time device code. Empty: SPIR-V, JIT at first start. |
I8_ESIMD=OFF |
Build without the fused int8 ESIMD expert kernels (stubs; the prompt's experts take the slower XMX path, answers do not change). |
ALLOW_BAD_OCLOC=1 |
Build although this ocloc is known to crash on the Battlemage kernels |
SPIN_MAX=N |
The bound on a device spin (STRATA_SYCL_SPIN_MAX); see Things that matter |
--pull matters because the image starts from a community image whose tag moves, and docker build reuses an old copy unless
told to fetch the current one. An old copy can hold a device compiler (ocloc from Compute Runtime 26.22.38646, IGC 2.36.3) that
crashes on the Battlemage kernels (IGC: Internal Compiler Error: Floating point exception, then gen compiler command failed with exit code 245, #3219). sycl/tools/build.sh checks the version and stops with the fix.
How to run it by hand
A greedy test run, inside the strata-sycl-dev image, AOT build in build-sycl-aot/:
STRATA_VERIFY_DEVICE_PLAN=1 STRATA_VERIFY_NO_HOST=1 \
build-sycl/strata --pack <iq pack> --native <shard1> --ple-gguf <shard2> \
--expert-profile data/expert-profile-coder.bin --expert-cache auto --stream-experts \
--prefill auto --spec 4 --spec-min-p 0.5 --max-context 8192 --tokens <ids> --max-new 64 --greedy
| Flag or variable | Meaning |
|---|---|
--stream-experts |
No resident host copy of the experts: every one goes from the GGUF into the VRAM cache through a small staging ring. The engine runs on 23 GiB of RAM; upstream needs 32 GB for this model. |
STRATA_VERIFY_DEVICE_PLAN=1 |
The GPU plans each layer itself |
STRATA_VERIFY_NO_HOST=1 |
The host waits for the whole window graph instead of per-layer rings. Required on xe whenever part of the experts is not in VRAM. |
--no-prefill-borrow |
The prompt path does not lend expert slots. The port borrows by default only above a 32K context (--prefill-borrow / --no-prefill-borrow decide explicitly). |
With --tokens-file instead of --tokens for long prompts (80K ids exceed Linux's single-argument limit). With a native pack
--prefill-until N skips the token loop, so compare output tokens between paths, never only timings.
Serve the port with serve/server.py --engine strata and sycl/serve/strata-sycl.sh as the exe, which starts the binary inside
the oneAPI runtime image with the serve pipes attached. Paths in the config's args are the container's. A config:
{"engine": "strata", "exe": "<repo>/sycl/serve/strata-sycl.sh",
"args": ["--pack", "/work/pack", "--native", "<shard 1>", "--ple-gguf", "<shard 2>",
"--expert-profile", "data/expert-profile-coder.bin", "--expert-cache", "auto", "--stream-experts",
"--prefill", "auto", "--spec", "4", "--spec-min-p", "0.5", "--mtp", "/work/mtp/rt",
"--max-context", "32768", "--kv", "int8", "--vram-reserve-mib", "1024"],
"sampling": {"temperature": 0.6, "top_p": 0.95, "top_k": 20, "repetition_penalty": 1.05}}
A native (IQ) pack takes --spec 2 at the smallest: the engine refuses --spec 0 and --spec 1 on one (it needs --native SHARD1, --spec T
with T >= 2 and --prefill CHUNK), so speculative decoding cannot be turned off for these packs.
With --stream-experts under STRATA_VERIFY_NO_HOST=1 (the strata-sycl.sh default) there is no SSD expert tier: every expert
must be in VRAM or in the pinned RAM mirror (STRATA_MIRROR_MIB), and the engine refuses to start when the two do not cover the model
("REFUSED: N experts are neither in VRAM nor mirrored"). Raise the mirror or lower --max-context. So the reserve matters: on the B70
with Coder IQ1_M 1,024 MiB fits all 12,288 experts with 2 GB of VRAM free.
Things that matter on this GPU
SYCL_CACHE_PERSISTENTmust be 0. The persistent JIT cache segfaults on Xe2 during the first compile. The start script sets it.- Do not ask for more VRAM than the card has. On the
xedriver an allocation past VRAM does not fail: the driver evicts buffers into host RAM, the kernel runs out of memory, and the machine can livelock until its hardware watchdog resets it. Compute the KV size first and leave about 1.5 GB free. - Thinking. The model reasons before it answers. Put
{"reasoning_effort": "none"}instrata-<model>.shared-settings.jsonnext to the config, or sendchat_template_kwargs: {"enable_thinking": false}per request. Otherwise a shortmax_tokensis spent inside the think block and the answer looks empty. /healthsays 503 while loading; the server polls/propsinstead.- Bounded spins. A device spin that never sees its flag makes the
xedriver time out the queue and reset the GT, and the card can stay wedged until a reboot. Every spin is capped. The cap is the build optionSTRATA_SYCL_SPIN_MAX(2,000,000 reads, a few seconds, unless the build is an AOT build for abmgcard, which keeps 20,000). - JIT builds add
-ze-opt-greater-than-4GB-buffer-requiredso kernels keep 64-bit addresses past 4 GiB into an allocation. oneMKL's own precompiled kernels do not get it; the GEMM stages an operand past 4 GiB (STRATA_SYCL_GEMM_STAGE=1forces that on Battlemage). - Aliased pages in big allocations (xe). The driver can return a large arena with two 2 MiB pages mapped onto the same
memory. Every allocation of 32 MiB or more goes through
strata::malloc_device_guarded, which checks the range and allocates again behind a spacer until it is clean (a warning names each retry; under 0.5 s at startup;STRATA_ARENA_ALIAS_CHECK=0skips it).STRATA_VERIFY_ALL_SLOTS=1reads every filled cache slot back and compares it with the GGUF. - Monitor tab.
server_intel.pyplugssycl/serve/xe_telemetry.pyinto the Monitor (temperature, power, PCIe link from sysfs). Load and VRAM come from/run/gpustat.json, written by a small root sampler (sycl/tools/gpustat.py, installed withgpustat.service), becausexereports them per client in/proc/*/fdinfo, which only root can read. A run config may name amodel_switcher; the web app's header then gets a Model menu. - Logprobs.
/v1/chat/completionstakes"logprobs": trueand"top_logprobs": 0..20throughserver_intel.py, streamed or not. Not on/v1/completionsor the Anthropic endpoint.
How much context fits
Only 12 of the 48 layers are full attention, so the KV grows slowly. KV streaming (--kv-resident) keeps the whole KV in pinned
host memory and only the attended window in VRAM; setup turns it on from 64K and keeps INT8 KV. --kv k8v4 does not support
streaming: setup keeps its KV in VRAM, which pushes more experts into the host mirror.
Measured on: Arc Pro B70 32 GB, Coder IQ1_M, engine 0.1.32-sycl, setup's flags (
--kv-resident 32768 --vram-reserve-mib 2048 --prefill 4096, the prompt borrowing cache slots), the same text repeated to length, 256 greedy tokens.
| Context | KV | Prompt | Decode after | Peak VRAM |
|---|---|---|---|---|
| 128K | int8 | 888 tok/s (144 s) | 66.6 tok/s | 29.9 GB |
| 128K | q4_0 | 848 tok/s | 62.8 tok/s | 29.7 GB |
| 128K | k8v4, KV in VRAM | 1,006 tok/s | 66.8 tok/s | 30.7 GB |
| 256K | int8 | 757 tok/s (338 s) | 55.9 tok/s | 30.2 GB |
| 256K | q4_0 | 737 tok/s | 51.7 tok/s | 30.0 GB |
| 256K | k8v4, KV in VRAM | 823 tok/s | 52.2 tok/s | 30.9 GB |
Borrowing keeps every expert in VRAM (the prompt buffers live in lent cache slots, refilled after the prompt). Without it
(--no-prefill-borrow) about 1,000-2,300 experts move to the pinned host mirror: the prompt is 2-5% faster and decode after it
10-23% slower.
Arc Pro B70 with a model that does not fit: Flash-Next IQ3_S
IQ3_S has 24,576 experts of 1.97 MB (46 GB). The 32 GB card holds about 11.5k of them (22 GiB of cache) and the rest sit in a pinned host mirror that the GPU reads over PCIe. Every prompt chunk streams the experts the cache does not hold, so a 4K prompt moves about 26 GB; bigger chunks read faster.
setup_intel.pywrites--prefill 4096for a card with 24 GB or more (the chunk's buffers take about 700 cache slots).--prefill 8192 --prefill-borrowlends cache slots instead and keeps all of them for decode, at about 1 s of refill per prompt. Setup recommends 8,192 for a Battlemage card with 24 GB or more above 32K context.STRATA_PREFILL_FIRST(a short first chunk that starts the GPU while PLE rows are read) defaults to 256 tokens only when the cache holds every expert, else 0.STRATA_VERIFY_NO_HOST=1is required whenever part of the experts is not in VRAM (strata-sycl.shsets it). Without it the host/GPU handshake is not visible across the bus onxeand the logits turn NaN a few tokens in.- On
xe, setup writesdata/expert-profile-sycl.bin, a ranking built from the routing of 45 varied prompts, so the cache holds the experts a chat, code or non-English prompt uses (tools/make_profile_mix.py,tools/collect_profile_traces.py). Ani915card keeps the shipped profile. - The prompt's expert copies run on the card's copy engine on Battlemage (
STRATA_COPY_ENGINE=1forces it elsewhere,=0turns it off). The experts in the last 10% of the cache slots are also mirrored so that lent slots refill by DMA (STRATA_LEND_MIRROR=0off).
Measured on: Arc Pro B70 32 GB (xe, kernel 7.0, oneAPI 2026.1, PCIe 13.3 GB/s host to device), 60 GB RAM, Flash-Next IQ3_S, 11.5k experts resident, greedy,
--spec 4 --spec-min-p 0.5 --mtp, INT8 KV,--stream-experts --vram-reserve-mib 1024 --ple-io ram, engine 0.1.40-sycl to 0.1.43. The 10-promptmg_norepeatgate passes 10 of 10.
| Workload | Result |
|---|---|
4,095-token prompt, --prefill 4096 |
980-1,002 tok/s |
8,169-token prompt, --prefill 8192 lending cache slots |
1,117 tok/s (9.4k slots) |
| Decode, 200-token story (66% of drafts accepted) | 30.5 tok/s |
| Decode, 200-token code answer (90% accepted) | 40.8 tok/s |
| 128K config, 4K prompt incl. refill (0.1.43) | 3.3 s (was 7.2 s); 16K 13.5 s (was 25.5); 49K 44 s (was 78) |
--prefill 8192 against 4096, 16K / 32K prompt (IQ3_S, setup's 128K flags) |
1,347-1,362 against 1,222 tok/s (+10-11%); 1,361-1,383 against 1,187 (+15-16%); decode after unchanged |
Fused int8 expert prompt path (auto) against FP16 chain, 1,024 / 4,095 tokens |
783.5 against 645.8 (+21%); 1,407.7 against 1,257.0 (+12%) |
Draft length (200 tokens, --spec-min-p 0.5): --spec 6 against --spec 4 is +6.5% on code and -1.5% on prose, so the setup
default (4, 0.5) stays. A --prefill 8192 chunk is not bit-identical to 4096 (first-token KL 1.1e-2 at 16K, top-1 the same).
Fused int8 expert path (Battlemage default). On a Battlemage GPU (B70, B580, B570, detected by device architecture) the
engine computes the prompt's routed experts with ESIMD kernels that decode the quantized weights in registers into the s8 DPAS B
operand and multiply them by q8_1 activations. Covered: gate/up IQ3_S, IQ3_XXS, IQ2_S and down IQ4_NL, Q2_0. Other formats keep
the FP16 path, as does every other GPU. STRATA_PREFILL_MMQ unset is auto, which times both chains at the first layer of each
format and takes int8 below the measured crossover (130-250 rows per expert); =0 turns it off, =1 forces it. Activations are
quantized, so bits differ from the FP16 path (KL 2e-4 nats mean over 6,002 positions, top-1 99.7%). Details:
INTEL_INT8_EXPERTS.md.
Arc A750 and the other Alchemist cards
An Alchemist card (A750, A770 and so on) has no FP64 hardware, and the A750 runs on the i915 driver. ./setup.sh --backend sycl
writes a different config for an i915 card, and a 300 MiB VRAM reserve and --draft-vocab en for any card under 12 GB.
Arc Pro B70 (xe, 32 GB) |
Arc A750 (i915, 8 GB) |
|
|---|---|---|
| Experts | --stream-experts: from the GGUF into VRAM, the rest in a pinned RAM mirror |
No --stream-experts: all of them in a RAM arena (about 40 GiB for IQ3_XXS), a few hundred in VRAM, the CPU computes the others |
STRATA_VERIFY_NO_HOST |
1 (the wrapper's default) | 0, written to the config's "env" |
--vram-reserve-mib |
1024 / 2048 | 300 |
--ple-io |
ram |
ram |
| FP64 | Native | Emulated: IGC_EnableDPEmulation=1, OverrideDefaultFP64Settings=1, NEOReadDebugKeys=1 in "env" |
| Build | AOT bmg-g31 |
JIT (no AOT=) |
- Why no
--stream-expertson the A750. The mirror is one pinned host allocation, and a singlesycl::malloc_hostabove a few GB fails on the A750. Without the flag the engine loads the experts into an ordinary RAM arena and the CPU computes the experts that are not in the 0.67 GiB cache. That needs about 43 GB of RAM for IQ3_XXS plus the 29 GB n-gram table with--ple-io ram. Setup warns when the RAM is less than the model's. - Why
STRATA_VERIFY_NO_HOST=0. With it set the GPU plans each layer itself and the CPU's experts are never asked for; the window hangs (Fence expiration time outin the i915 log). - FP64. One kernel on the sampled path declared
double; the sampler's tail now accumulates in float (-DSTRATA_SYCL_SAMPLER_FP64=1keeps double). Setup still writes the three emulation keys for an A-series card, because other kernels that use double were not exercised without them on long contexts or with vision. On the A750 a sampled decode ran at 12.0 tok/s without the emulation keys against 9.8 with them (median of 4 interleaved pairs). - Prompt GDN recurrence runs a sub-group kernel on Alchemist (variant 6,
STRATA_GDN_XE): on by default there,=0off,=1on any Intel GPU with 16-lane sub-groups. On Battlemage it stays off (-3.5%). - Grouped XMX expert launch (
STRATA_PF_XMX=2) is the default on Alchemist: the prompt path decodes and multiplies a group of experts in one fused launch on the XMX units.=0turns it off;=1dequantizes first and runs one grouped GEMM per product. Battlemage keeps the older path (the same kernels are 7-11% slower there). - Streaming copies. Only experts in USM host memory (the
--stream-expertsmirror) are copied directly; every other expert goes through the stager's pinned buffers, because SYCL has nocudaHostRegisterand a direct copy out of pageable memory could hang a prompt.STRATA_PF_DMA_PAGEABLE=1restores the direct copy. - The engine segfaults in a worker thread of the CPU pool when it exits (dmesg only, after the server printed "stopped"). It is not a GPU event.
Measured on: Arc A750 8 GB (i915, PCIe 4.0), Ryzen 5 5600X (AVX2, no AVX-512), 64 GB RAM, Flash-Next IQ3_XXS, default flags, engine 0.1.45.3.
| Workload | Result |
|---|---|
| Prompt, 1K / 2K / 4K / 6K tokens, grouped XMX launch | 46.4 / 83.1 / 110.4 / 114.8 tok/s (before: 35.1 / 61.7 / 79.3 / 66.3) |
| Decode | 18.7 tok/s (unchanged by the grouped launch); 10-15 tok/s in the first report |
| PCIe probe | 10.6 GB/s |
| Quality of the grouped launch against the default | teacher-forced KL 0.0002 over 6,006 tokens (default against itself 0.0001), first-token KL 0.0017, top-1 99.7%; HumanEval (every 2nd, 82) 73, 69, 74; needles at 2K-6K 9/9; 8K-context tool-call agent 6/6 |
Gyro on an Arc. Gyro-S and Gyro-M (trellis-coded experts, GYRO.md) run on the SYCL engine. They were
tested on the A750 only. Setup writes the usual A750 config (32K context, 300 MiB reserve) without --ple-io ram, because the
28 GB n-gram table stays on the SSD while the expert arena takes 24 GB of RAM (Gyro-S). Gyro-M needs --max-context 8192 --vram-reserve-mib 100 --prefill 1024 on 8 GB. A second Arc card does not take Gyro's experts (the engine refuses and says so).
The Battlemage AOT build compiles with the trellis kernels and was not run.
Measured on: Arc A750 (i915, 8 GB), Ryzen 5 5600X, 62 GB RAM, Gyro-S, engine version not recorded.
| Workload | Result |
|---|---|
| Prompt, 1,406 tokens | 82 tok/s (40 with dequantize + oneMKL) |
| Prompt, 2,377 / 20,000 tokens | 102 / 195 tok/s |
| Decode, MTP draft of 4 | 15-16 tok/s |
| Experts in the card's cache | 410 of 24,576 |
| Gyro-M, 8K context | decode 14 tok/s, prompt 62 tok/s |
Decode is bound by the CPU pool and the PCIe link (10.4 GB/s), not by the card's kernels. --prefill 1024 gave +13% decode and
-24% prompt speed; the default stays --prefill auto.
Pictures on an Arc
Pictures follow the rule of every other card (MODELS.md): the picture reader runs on the CPU under 24 GB
of VRAM and on the card from 24 GB. strata_control/vision_reader.py is the one place for that rule. On an Arc, pictures are on by
default with the CPU reader, a B-series card included. An Arc of 24 GB or more also keeps the CPU reader; --vision gpu asks for
the GPU reader.
- CPU reader:
strata-vision(llama.cpp's mtmd). Setup compiles it intoengine-sycl/, the server runs it on the host, and the engine in the container reads the embeddings from the host's/dev/shm, whichstrata-sycl.shshares (STRATA_SYCL_SHM=0keeps a private one). - SYCL reader:
strata-vision-sycl, the same program on llama.cpp's SYCL backend.setup --backend sycl --vision gpucompiles it on the host when the oneAPI compiler (icx,icpx,intel-oneapi-mkl-devel) is installed; otherwise setup says so and keeps the CPU reader. It needs the PC's oneAPI runtime, not the container. If it does not start, the server falls back to the CPU reader with one log line. It takes VRAM the experts would use, which is why the rule gives it to 24 GB cards only. Nobody has run it on a card of 24 GB or more. - Known risk on Battlemage. The first picture on the Arc Pro B70 (engine 0.1.45, IQ3_S, CPU reader) logged an xe engine
reset (ccs + bcs) and
Fault response: Unsuccessful -EINVAL, and the request ended 503. It has not been looked at since. Setup andstrata launchprint one plain line that the card is new to Strata and how to report a problem.--vision no(setup) or--vision off(strata launch) turns pictures off.
Measured on: Arc A750 (8 GB), IQ3_XXS, 8K context, JIT build, 448x448 picture.
| Reader | Time per picture |
|---|---|
CPU (strata-vision) |
1.73 s |
SYCL (strata-vision-sycl), after a 30 s first start that compiles the kernels |
0.16 s |
The embeddings match the CPU reader's (cosine 0.9959 to 0.9999 over 8 pictures). A test picture (red circle, blue square, the words "STRATA 42") was answered correctly by both readers 10 of 10; two pictures in one request, a picture kept in a second turn's history, and text requests after the pictures all answered, with no GPU fault in the kernel log.
The SYCL port (sycl/)
Layout. sycl/src and sycl/include mirror the tree and hold only the files the port changes; sycl/CMakeLists.txt takes every
other source from the original location. sycl/include/dpct/ is the vendored SYCLomatic helper library.
How it is made, so it can be redone.
sycl/tools/Dockerfileis the dev image: the llama.cpp SYCL image plus SYCLomatic (dpct2025.3), ninja, and the CUDA 12.8 headers thatsycl/tools/get-cuda-headers.shpulls from NVIDIA's pip wheels (dpct parses CUDA; it needs headers, not the toolkit).sycl/tools/migrate.shwrites a compilation database for the CUDA-touching translation units and runs dpct over them.sycl/tools/fixups.pyfixes what dpct got wrong, as an idempotent script with a reason per item. Examples: CUDA's null stream becomes the default stream (every stream cast goes throughstrata::q_of()),__ldgand__fadd_rntranslations that lost their meaning, ggml lookup tables as plainstatic constarrays.sycl/tools/build.shconfigures and builds with icpx inside the image. Two compiler flags are load-bearing:-fp-model=precise(icpx defaults to a fast model) and-cl-fp32-correctly-rounded-divide-sqrtfor the device compiler. The Arc's fp32 divide is not correctly rounded by default, and Strata's quantizers are byte-exact against ggml throughamax / 127.
What the port had to get right.
- Host-mapped flags:
volatiledevice loads do not bypass the caches on Intel; system-scope atomics do. Host-to-device visibility during a kernel stays unreliable, which is why the all-resident path waits for the window instead. - 32-lane sub-groups (
-fsycl-default-sub-group-size=32): the kernels are written for warps. - Synchronous copies:
cudaMemcpyblocks, dpct's default-queuememcpydid not wait. - Stager and PLE waits: no host thread waits on a queue's event under the Level Zero v2 adapter; the copy queue writes a sequence number into page-locked host memory and the stager polls it.
- Profiling:
sycl/tools/Dockerfile.unitracebuilds the dev image with Intel's unitrace;unitrace -daround the engine plussycl/rank_kernels.pygives device time per kernel.
Keeping up with upstream. A merge of upstream main leaves the copies in sycl/ behind wherever upstream touched a file they
mirror. They are refreshed by re-migration:
- Merge upstream. No shared file should conflict. Check that
sycl/setup_intel.py --checkandsycl/serve/server_intel.py --engine mockstill run against the newsetup.pyandserver.py. - Migrate the old upstream tree with
migrate.shintosycl-baseand the merged tree intosycl-new. - Run
sycl/tools/normalize.sh <dir> <commit>on both (dpct's file names, unchanged files, message serials, thenfixups.py). - For every file present in both:
git merge-file sycl/F sycl-base/F sycl-new/F. Copies dpct never produced (verify.cpp,mtp.cpp,remote_experts.cpp) take upstream's diff by hand; new parity tests are copied in and listed insycl/CMakeLists.txt. - A fixup whose pattern upstream changed shows up as a compile error; extend the fixup, re-run it, rebuild. Run a symbol audit (count the port's feature identifiers before and after).
sycl/tools/merge_upstream.py ports open upstream PRs ahead of upstream the same way.
Not ported. Three kernels carry inline PTX (mma.sync, ldmatrix, cp.async): qsa_prompt_attn, qsa_select's block
scores and native_qsa_score. The SYCL build takes the "older card" fallback the CUDA build uses below sm_80.
qsa_prompt_attn also has an XMX version (STRATA_PROMPT_ATTN_XMX=1) that is correct but slower than the fallback. The ggml MMQ
prefill path (moe_mmq.cu) is not built. --peer-device (a second GPU as an expert-cache tier) is stubbed and refuses with a
message. The sm_90 thread-block-cluster sampler and QSA top-k have no SYCL counterpart. Images on the llama.cpp config are not wired;
on the SYCL port they are (Pictures).
Switch reference
All are environment variables for the engine. Put them in the config's "env" block. Defaults are for the current engine.
| Switch | Default | Effect |
|---|---|---|
STRATA_VERIFY_NO_HOST |
1 (strata-sycl.sh); 0 on i915 |
See above |
STRATA_MIRROR_MIB |
Free RAM less 4 GiB | Size of the pinned host mirror for experts without a VRAM slot. Set 0 on a layer split where the mirror would not be used. |
STRATA_PF_XMX |
2 on Alchemist | Grouped XMX expert launch; 0 off, 1 dequantize first |
STRATA_GDN_XE |
On for Alchemist | Sub-group GDN recurrence |
STRATA_COPY_ENGINE |
On for Battlemage | Expert copies on the copy engine |
STRATA_PREFILL_MMQ |
auto on Battlemage |
Fused int8 prompt experts |
STRATA_PREFILL_STREAM_ALL |
On when VRAM holds more than 90% of the (layer, expert) pairs | Copy every expert the cache does not hold, layer by layer; =0 / =1 force it |
STRATA_PREFILL_FIRST |
256 if the cache holds every expert, else 0 | Tokens in the short first prompt chunk |
STRATA_LEND_MIRROR |
On | Mirror the experts in the last 10% of cache slots; =0 off |
STRATA_VERIFY_LEAN |
On | Decode graph without unanswered nodes; =0 off |
STRATA_FILL_SERIAL=1 |
Off | The old serial expert-cache fill (the pipelined fill is the default) |
STRATA_MMVQ_A2=0, STRATA_MMVQ_WIDE_32=0 |
On | Restore the older matvec kernels |
STRATA_MMVQ_LARGE_GRF |
On for Arc A-series | 256 registers per thread for the 2-8 column wide kernels |
STRATA_Q6_PLANAR |
On | Q6_K output head and MTP head as planes (same bytes) |
STRATA_ATTN_RS |
2 | QSA prompt attention probability layout (bit-exact variants) |
STRATA_IQ3S_WIDE=1 |
Off | Wide kernel for dense IQ3_S matrices: neutral on one B70 (decode -0.3%), the author reports gains on two cards |
STRATA_EXPERT_SPLIT=1 |
Off | Upstream's i-quant multi kernels (-11 to -16% decode here) |
STRATA_PROMPT_ATTN_XMX=1 |
Off | joint_matrix prompt attention (-24% to -27% prompt on the B70) |
STRATA_SYCL_GEMM_STAGE=1 |
Off on Battlemage | Stage GEMM operands past 4 GiB |
STRATA_I8_NO_ESIMD |
n/a | Compile-time: stubs for the int8 ESIMD kernels (set for a device without them) |
STRATA_DBG_NAN=1 |
Off | Report non-finite values, including the experts' FP16 inputs |
History / older measurements
Dated results from earlier engines. They show how the numbers moved and are not instructions. Narrative from the original work log (debugging stories, per-merge notes, planned-item lists) was dropped; the numbers and their labels are kept.
Measured on: Arc Pro B70 32 GB (xe), PCIe 3.0 x8, Ubuntu 24.04, Coder IQ1_M, 32K context, INT8 KV, the SYCL port, engine 0.1.31-sycl (2026-10-01), against llama.cpp SYCL (2026-09-29, q8_0 KV).
| NVIDIA (RTX 5070, Strata) | Intel Arc, SYCL port | Intel Arc, llama.cpp | |
|---|---|---|---|
| Decode, Coder IQ1_M | 44-51 tok/s | 78.2 tok/s (19-token prompt), 75.7 (2,184-token prompt); 48-72 through the API | 23-25 tok/s |
| Prompt | about 1,870 tok/s | 790 tok/s at 2,184 tokens, 1,117 at 40K, 888 at 128K, 757 at 256K | about 150 tok/s (424 at 105K) |
| Experts | in RAM, hot ones on the card | all 12,288 in VRAM (--stream-experts), about 1.9 GB VRAM free |
all of shard 1 on the card |
| Load time | about 26 s to first token (was 82 s before the pipelined fill) | about 100 s |
Decode, Coder IQ1_M on the B70 (19-token prompt, 256 greedy tokens), by change. Each step measured alone, output tokens identical unless noted.
| Change | Decode |
|---|---|
First working engine (suffix drafter only, --spec 2) |
20.2-20.8 tok/s |
With the base checkpoint's MTP draft layer (--spec 4 --mtp, 81-87% accepted) |
42.7-45.3 tok/s |
| After the 0.1.25-0.1.27 merge | 46.5 |
| Commit graph left running while the drafter runs on its own queue | 49.8 |
| Q6_K alignment fix (a Q6_K block is 210 bytes; two aligned 16-byte loads) | 53.9-54 |
resident_plan grouping in parallel |
57.1 |
| GR down kernel reads activations directly | 58.7 |
| Wide 16-byte-load kernels for Q4_K, Q5_K, IQ4_XS | 62.6 |
| Wide kernels for Q8_0 and IQ4_NL; Q6_K activation loads aligned | 65.6 |
| GR down projection sliced by columns | 70.6 |
| IQ4_NL expert dots with codebook in registers | 76.1 |
| GR norm per (token, stream); aligned weight loads in IQ3 gate/up dots | 77.9 (output differs after token 146) |
Prompt, same card, 2,184-token prompt: 560 tok/s (first engine) to 792 tok/s after dequant vector stores (0.085 to 0.030 ms per
expert), a short first prompt chunk (711 to 765 tok/s), one device module per kernel and the PLE reader at 64 threads
(466 to 330 ms for that prompt's rows). Draft sweep at 2,184 tokens: --spec 4 41.6-43.5 tok/s, --spec 2 33.6, --spec 6
35-37. Decode depends on the text: 45-51 tok/s on code (77-85% of drafts accepted), 30-39 on prose (55-72%).
Flash-Next IQ2_XS on the B70 (original model, 18,329 of 24,576 experts in VRAM, 6,247 in the pinned mirror). Decode 50.8 tok/s
on a 19-token prompt and 60.6 after 2,184 tokens, prompt 549 tok/s (0.1.31); after the 0.1.31 merge 58.6 / 64.2. A forced test
(--expert-cache 8000, 1,879 experts out of VRAM) took decode from 2.6 to 40.9 tok/s (full cache 43.3) once the host mirror existed.
Long prompts. 80,000 tokens at --max-context 81920 --kv int8 --vram-reserve-mib 2048 --prefill 4096 with slot borrowing:
75 s (1,062 tok/s), decode 35-40 tok/s after it; without borrowing, decode after the prompt ran at 2 tok/s. 128K int8 with
KV streaming: 897 tok/s routed-only against 920 with the stream-all walk; 40K 1,106 against 1,160; 128K k8v4 1,006 against 1,041;
256K 760 against 780 (0.1.38). Prompt time at 40K (GPU timeline): expert dequant 15.7%, expert GEMMs 23.4%, prompt attention
19.2%, host grouping 6.5%, QSA select 6.8%, DeltaNet 5.8%.
Context sizes on llama.cpp (2026-09-29, Coder IQ1_M, q8_0 KV, 32 GB card). About 20 KiB of KV per token.
| Context | VRAM in use | Status |
|---|---|---|
| 32,768 | 28.4 GB | Measured, the default |
| 65,536 | 29.0 GB | Loads |
| 131,072 | 30.3 GB | Practical ceiling: a 104,798-token prompt read in 247 s at 424 tok/s |
| 262,144 | about 32.6 GB | Does not fit; asking for it took the host down |
Load time. The expert-cache fill became a pipeline (reads in file order by up to 8 threads into page-locked batches of 64, copies overlapped): a cold fill of the Coder's 23.4 GiB went from 76.2 s to 18.8 s (0.33 to 1.34 GB/s), a whole start from 82 s to 26 s, and the IQ2_XS from 120 s to 41 s.
Upstream PRs ported early (0.1.35).
| PR | What | On the B70 |
|---|---|---|
| #385 | Stager: a buffer's first job of a generation waits for the previous DMA | Race fix, same output |
| #463 | Decode waits for an adaptive swap before reading the residency table | Determinism fix |
| #453 | Drafter's batched K/V on the KV-streaming ring | 128K int8: 888 to 895 tok/s prompt, 66.6 to 67.0 decode |
| #374 | First chunk's PLE rows read beside layer 0 | Part of 2,184-token prompt 784 to 825 tok/s |
| #363 | PCIe expert call: group stride, fused SwiGLU + q8_1 | IQ2_XS decode +1.7% |
| #407 | --adapt-tuned (opt-in) |
Neutral |
| #413 | DeltaNet recurrence per key head | 8% slower prompt: off (STRATA_GDN_KEYHEAD=1) |
Alchemist prompt GDN kernel (Arc A770 16 GB, xe, oneAPI 2026.1 JIT, 2026-10-13). gdn_parity --prefill-tokens 8192 51.0 to
24.5 ms; 32,768 tokens 204-236 to 110-113 ms. End to end, Coder IQ1_M, --max-context 131072 --kv int8 --kv-resident 20480 --vram-reserve-mib 1200 --resident-experts: prompt 386 to 396-407 tok/s at 8K and 478-480 to 511-518 at 32K. To check another
Intel card: STRATA_GDN_XE=1 ./gdn_parity --selftest --prefill-only.
Q6_K decode kernels (0.1.45, #2340, #2207). B70, mmvq_bench Q6_K 2560x10240, 1 column 44.7 to 26.5 us, 4 columns 64.5 to
46.2; A750 4 columns 251 to 102 us. End to end on the B70 (IQ3_S, 5 interleaved pairs, answers identical): decode 32.24 to 33.37
tok/s on a story prompt (+3.5%), 39.11 to 40.77 after a 4K prompt (+4.2%).
Fused int8 expert prompt path (B70, IQ3_S, 32K config, fixed flags, 5 interleaved rounds). 1,024 tokens: default 645.8, =1
775.4 (+20%), auto 783.5 (+21%). 4,095 tokens: 1,257.0, 1,413.8 (+12.5%), 1,407.7 (+12.0%). At more than about 130 rows per
expert (a chunk of 8,192 tokens or more) the int8 chain stops winning (1.03x at 194 rows against 1.4-1.9x at 60).
Other prompt-path trials on the B70 (4,095-token IQ3_S prompt, tok/s).
| Change | Result |
|---|---|
STRATA_GDN_XE=1 on Battlemage |
1,251 to 1,207 (-3.5%) |
STRATA_PROMPT_ATTN_XMX=1 / =32 |
1,258 to 952 / 913 |
STRATA_ATTN_RS=2 (now default) |
1,254 to 1,265 (+0.9%) |
STRATA_ATTN_RS=1 |
1,254 to 1,236 (-1.5%) |
| Int8 expert groups of 24 or 32 | No effect |
Grouped XMX expert GEMM on Battlemage (STRATA_PF_XMX=1/2) |
11-15% slower |
| Miss-first / hit-first group order in the device plan | -3% / -7% |
Via the OpenAI API, sampled (0.1.31-sycl era). 36 tok/s decode on a first turn, 33 on a follow-up, against 24-26 for llama.cpp on the same card. Logprobs for "Paris" (IQ2_XS) -0.0019 (p 0.998).
Tests (0.1.x ports). On the B70, 19 of the 22 kernel parity tests passed byte for byte or within tolerance; the others needed
model fixtures or the tensor-core kernel. Parity gaps that remain: s2_expert_grouped_parity (the s2 path), kv_hybrid_parity's
last step on the XMX kernel, ple_parity and native_expert_parity need model files.
Fixed bugs worth knowing if you see the symptom (fixed long ago; the symptom is listed so a search finds this line). --stream-experts read
the wrong shard for layers past a shard boundary (NaN logits from layer 13 on, Swift 1.5). IQ2_XS products were garbage because of
a type pun the SYCL compiler does not honour. A prompt that borrowed cache slots hung in its first chunk (stager deadlock under
Level Zero v2). An A750 prompt of 2,744 tokens hung (direct copy out of pageable memory; regression test
sycl/tools/test_prompt_deadlock.sh). The first request after a start returned token 0 on i915 when the spin bound was too short.
An A770 JIT build misread memory past 4 GiB into an allocation. The xe driver aliased pages in a 22 GiB arena (NaN at layer 4).
Mixed-IQ3 decode kernels for Battlemage (PR #4185)
Packed Q2 / IQ4 lookup / IQ3_S sign arithmetic (the build option) and fourteen decode controls. Measured on two Arc Pro B65
(Qwen3.8-Flash-Next GSQ-RCO IQ3_S, all experts resident, layer split, --spec 2 --pipeline-windows 2, int8 KV, AOT bmg-g31): decode
+9.7% on a 4K prompt and +11.1% on a free-form answer against the build without them, same generated tokens, same logprobs to six
decimals. Prompt speed is unchanged.
STRATA_SYCL_IQ_DECODE_FAST(CMake, default ON;IQ_DECODE_FAST=OFFforsycl/tools/build.shandpackaging/build_sycl_linux.sh): integer arithmetic with the same results as the helpers it replaces. It is the largest part of the gain (+5.9% alone).- Runtime controls. Unset: on for a Battlemage device, off for every other device (Arc A-series keeps the earlier kernels).
STRATA_<name>=0turns a control off,=1turns it on whatever the device. Restart after changing one.
| Runtime control | On by default (Battlemage) | Selected work |
|---|---|---|
STRATA_MMVQ_SHAPE_RPW |
yes | Row reuse for bounded Q4_K/Q5_K projection shapes |
STRATA_IQ3S_GU_ROW_REUSE |
yes | Reuse IQ3_S gate/up row inputs across two rows |
STRATA_IQ2S_GU_ROW_REUSE |
yes | The same for IQ2_S |
STRATA_IQ3XXS_GU_ROW_REUSE |
yes | The same for IQ3_XXS |
STRATA_GR_LO_REUSE |
yes | Reuse the hyper-connection low-rank SiLU output on supported layouts |
STRATA_ATTN_MERGE_ACTIVE |
yes | Merge only the active chunks in bounded batched QSA decode |
STRATA_IQ3S_GU_SLM |
no | Stage native IQ3_S gate/up activations (unreachable while the IQ3_S row reuse is on) |
STRATA_IQ4NL_DOWN_SLM |
no | Stage native IQ4_NL down activations |
STRATA_EXPERT_ROW_E3 |
no | Three-entry tails in the IQ2_S / IQ3_XXS row-reuse path |
STRATA_RESIDENT_PLAN_COMPACT |
no | Size the resident routing work-group for the entry count |
STRATA_GR_CACHED_NORM |
no | Reuse the hyper-connection normalized input on supported layouts |
STRATA_GR_REDUCE_ILP |
no | Keep partial loads in flight in the cached-lo hyper-connection reduce |
STRATA_Q6_SHAPE_GRF |
no | Large GRF for bounded Q6_K projection shapes |
STRATA_GDN_QK_REG |
no | Register-broadcast QK for supported Battlemage GDN geometry |
The eight "no" controls measured neutral or slightly slower on that configuration (single-control table in
phaseA-tests/pr-priorities/RESULTS-INTEL.md); they stay available for other models and cards. Unsupported layouts retain their
existing paths. Explicit projection overrides take precedence over shape selection.