# Kuno development gateway Kuno's first implementation provides a local Ollama reverse proxy, conservative request admission, an authenticated control listener, persistent usage counts, and an installable systemd gateway with reversible Ollama port takeover. This default service remains in gateway-only mode. An opt-in native Ollama GPU-worker path is implemented and hardware-tested. An optional experimental native Qwen image path is under validation; see [image development](../../docs/kuno-images.md). The [specification](../../docs/kuno-spec.md) defines the full product and milestones. ## Install on this Linux host Build a native release package: ```sh ./apps/kuno/scripts/package-release.sh ``` Install with: ```sh sudo ./dist/kuno-linux-x86_64/kuno install ``` Installation warns that inference will briefly be interrupted, then waits for `Continue? [y/N]` before changing services. Enter or `n` cancels. Connected clients can reconnect afterward; you do not need to close them first. The installer checks actual listener ownership, native executable identity, effective environment, and competing services before changing anything. It stops Ollama's runner group during cutover rather than trying to infer idleness from open TCP connections. `install`, `upgrade`, `configure-managed`, `recover`, and `takeover-restore` all require this confirmation. Unattended scripts must explicitly pass `--yes`; redirected input does not imply consent. The website installer should display the interruption warning and obtain confirmation before invoking an unattended command. Installation includes health checks. For a separate diagnostic report later, run `sudo /usr/local/lib/kuno/kuno doctor`. The installer preserves the actual IPv4/IPv6 listener, moves Ollama to a free loopback port, installs Kuno using an unprivileged systemd `DynamicUser`, and enables Kuno at boot. Kuno starts paused; only after both listeners and upstream health are verified does installation activate admission. Subsequent boots wait for upstream health and use a root-controlled activation marker. The original loaded-model ceiling remains effective for the original Ollama service. Optional managed workers use Kuno's separate ceiling and GPU admission checks. Install/restore require sudo; serving inference does not. Supported installations use a running native `ollama serve` systemd service with root-controlled unit and executable files. Containers, environment files, differing wrapper environments, and ambiguous listeners receive diagnostic errors. `--service NAME.service` selects a different native Ollama service. The current default host has a dual-stack `[::]:11434` listener even though `OLLAMA_HOST` says `0.0.0.0:11434`. Cancellation recovery (also briefly interrupts inference): ```sh sudo /usr/local/lib/kuno/kuno recover ``` This explicitly restarts the managed gateway and Ollama, clears the empty in-flight marker only after Ollama's runner group stops, and retains usage data. It unloads Ollama's resident models. It never restarts unrelated GPU applications. Undo installation, or recover an interrupted installer, using the release binary: ```sh sudo ./dist/kuno-linux-x86_64/kuno takeover-restore ``` Keep that release binary available in case restoration has already removed the installed copy. Restore disables Kuno, removes unchanged owned files, and returns Ollama to its original listener and active state. Usage data and the protected setup journal remain. Restore stops both services before clearing uncertain runtime work; clients can reconnect after Ollama returns to its original port. Changed installed files are not overwritten. Re-running the same install is idempotent. To update, run the new downloaded binary with `sudo ./kuno upgrade`. It asks once for the whole replacement, verifies the existing owned files, preserves configuration and usage history, and restores the previous artifacts if the new version fails activation. A process crash still uses the existing recoverable restore journal; keep a release binary available. See [packaged installation instructions](packaging/README-install.txt) for paths and content-free diagnostic commands. On 2026-09-22, installation and privileged post-install diagnostics passed on the target Linux Mint host. Both IPv4 and IPv6 forwarding matched upstream metadata, and the service is enabled at boot. Operator recovery and restore/reinstall also passed on 2026-09-23. The original IP binding was verified remotely from 192.168.1.74, including streamed generation after recovery and successful admission after cancelling a live generation. The installed fix drains cancelled responses and records boot identity; its reboot handling has automated coverage. Live upgrades preserving managed GPU configuration have also passed; a full managed-service reboot test remains. ## Resource observations (M2 foundation) The new binary provides a read-only snapshot: ```sh ./dist/kuno-linux-x86_64/kuno resources ``` It uses the installed `/etc/kuno/config.json` when present, or accepts an explicit `--config FILE`. The authenticated control API exposes the same report at `GET /kuno/v1/resources`. Monitoring does not change usage eligibility or load, unload, or generate with models. The report is not persisted. PCI addresses identify physical device locations, with a NVIDIA UUID when exposed. Intel Xe VRAM uses the same read-only DRM memory-region query proven in the local itop project. Ambiguous zero usage is reported as unavailable, never free memory. NVIDIA uses its installed NVML driver library directly, preferring the v2 counters that distinguish used, reserved, and free memory. The v1 fallback explicitly marks that used memory includes driver reservations. AMD sysfs support is implemented but has not been validated on AMD hardware. No monitoring CLI or new runtime package is required on the tested NVIDIA/Intel host. Generated service units include the existing `render` group when available so an unprivileged Kuno process can query DRM render nodes. This grants no root or performance-monitoring capability. Missing permissions, restricted counters, unsupported drivers, and failed probes remain explicit errors; their capacities are not guessed. A native probe has a three-second response deadline and a single in-flight probe limit. A timed-out probe retains its slot until it exits, so repeated monitoring cannot accumulate blocked driver threads. Probes use a standalone thread so runtime shutdown does not wait for that worker. Validated on the development host with the Arc Pro B70 (`xe`) and RTX 3060 (NVML), including the Intel memory query inside a transient service using `DynamicUser=yes`, `SupplementaryGroups=render`, and the gateway's sandbox settings. The check does not require restarting the installed gateway or Ollama. Ollama `/api/ps` reports are classified as GPU-resident, partially offloaded, CPU-resident, or unknown **according to Ollama's byte counters**. They do not prove which physical GPU holds a model or where every computation executes. No external GPU consumer is blamed or blacklisted based on these observations. The default installation keeps scheduling disabled and preserves the original Ollama limit. The internal `placement` module now plans across backend-managed workers: exact warm reuse, another available GPU, then LRU eviction of idle verified loads. Plans account for peak workspace, headroom, external allocations, runtime/driver and settings identity, and every GPU/worker touched by a split-model eviction. Unknown telemetry is not capacity. Eviction always requires a fresh measurement before loading. The opt-in `managed` configuration now executes single-device plans through owned Ollama workers and durable GPU/worker reservations. ### Experimental managed inference A configuration may include `managed: {"reserve_bytes":1073741824,"workers":[...]}`. Each worker has absolute `executable`, shared `models`, private writable `home`, a unique loopback `address`, `max_loaded_models`, `context_length`, and a `device`: `{"backend":"cuda","uuid":"GPU-...","pci_address":"0000:04:00.0"}` or `{"backend":"vulkan","index":1,"pci_address":"0000:2d:00.0"}`. Device indexes are host-specific. Startup verifies Ollama's discovered PCI identity, and CUDA UUID, before loading. A changed enumeration fails closed. This is currently a development configuration. Installed services can apply it transactionally with `sudo kuno configure-managed --workers /absolute/path/to/workers.json`. That file contains the `PoolConfig` object (`reserve_bytes` and `workers`), not the full gateway configuration. `sudo kuno configure-managed --disable` restores gateway mode. Both commands warn and wait for confirmation, or accept an explicit `--yes` for an already-authorized unattended invocation. They preserve the listener, upstream, credentials, original Ollama settings, and gateway limits. The installer assigns worker homes within Kuno's private state directory and generates a scoped read-only model mount. Serving uses that mount instead of requiring access through a user's private parent directories. Each worker must verify its physical GPU during startup before the gateway opens its listeners. Failed configuration activation rolls back the unit, configuration, and binary together. Later upgrades retain the current managed configuration. Hardware selection remains explicit until automatic discovery/configuration is ready. Native `/api/chat`, `/api/generate`, `/api/embed`, and `/api/embeddings`, plus `/v1/chat/completions`, `/v1/completions`, and `/v1/embeddings`, use the workers. Ollama performs its existing compatibility conversion and SSE formatting. Those compatibility endpoints use worker/model context defaults, as Ollama does; native `options` supplied to a compatibility request do not influence admission. Verified cloud tags use the original Ollama. Unknown models, other compatibility APIs, and opaque operations use the original gateway path after stopping owned workers; those fallback paths do **not** guarantee GPU-only execution. Global admission still serializes inference across both cards. `/api/ps` aggregates worker residency. Status reports `managed_experimental`; the global GPU-placement guarantee remains false. Workers set their own finite loaded-model ceiling (tested with eight), independent of the catalog owner's original one-model limit. Preloading verifies the artifact digest, context, and full GPU residency before sending user input. Unsupported memory-affecting request overrides are rejected. First use isolates a workload; subsequent requests can use a RAM-only load-footprint observation plus workspace allowance and headroom. This is not a measured inference-peak profile or a learned safe-combination database. Unknown/external memory is not credited or blacklisted. The returned job retains reservations through response completion, including bounded draining on client disconnect. Uncertain completion quarantines its worker; reconciliation stops and reaps the owned runtime before reuse. Durable uncertainty after process restart requires `kuno recover`, which stops the service and backend before clearing both admission and GPU-reservation markers. Systemd owns the worker process tree. Request bodies and engine stderr are not persisted. Failed placement now captures memory before cleanup, stops only the affected owned worker, and waits up to three seconds for a stable window of three unloaded readings. The lowest observed unloaded memory use is a **provisional** baseline: it includes desktop usage and may include unknown applications. Memory still above that baseline is classified as unexplained pressure; missing or unstable telemetry remains inconclusive. A return to baseline requires a controlled retest before blaming a combination, since an external allocation could have disappeared. No outcome creates a blacklist. For capacity/estimate failures with existing residents, a recovered baseline allows one isolated retry after cleanup. No useful inference has been dispatched at that point. Runtime/verification failures, unexplained pressure, and failed isolated attempts do not trigger that retry. The pool may still try another GPU. The authenticated `/kuno/v1/learning` endpoint exposes at most 64 failure records per worker, including artifact digests, context sizes, co-resident workloads, memory observations, and classified outcomes. This evidence stays in RAM and does not yet provide persistent profiles, idle retesting, or process attribution. Hardware tests on both CUDA/RTX 3060 and Vulkan/Arc Pro B70 verify pre-cleanup evidence, delayed memory release, one isolated retry, and successful loading with a corrected estimate. A separate test runtime holding GPU memory stays alive and loaded while Kuno classifies the remaining allocation as unexplained pressure. Hardware acceptance on this host passed for CUDA/RTX 3060 and Vulkan/Arc Pro B70: physical-device selection, two GPU-resident models, actual text and embeddings, eviction followed by memory remeasurement, abandoned-job recovery, and full native gateway routing with warm reuse and unload. A two-card gateway run also verified that a cold workload uses the empty alternate GPU while preserving the first model for warm reuse. Both GPU paths also passed generation, embedding, multiple-residency, and unload checks inside a restricted `DynamicUser` service. On this host, `/media/jmicko` grants traversal to the original `ollama` user but not the dynamic account. The acceptance service exposes only the shared model directory through `BindReadOnlyPaths` at a service-private path. Parent directory permissions were not widened. Installed managed deployment provides the same scoped access; joining the user's personal group is not required. Models were `lfm2.5-thinking:1.2b-bf16` at context 4096 and `embeddinggemma:latest` at 2048. A two-card development gateway additionally passed OpenAI-style chat, completions, embeddings, SSE completion, and draining an abandoned compatibility stream. The installed service passed generation and embedding across both GPUs and `ornith-1.5:35b` generation at context 128000 with full GPU residency on the Arc. An intentionally incorrect Vulkan index failed physical-device verification and restored the previous binary, unit, and configuration byte-for-byte. A parser regression test checks the model mount with a real user systemd manager without starting the temporary unit (`cargo test -p kuno --lib systemd_parses_model_mount -- --ignored`). The source and destination must be quoted separately so systemd does not interpret their separator as part of the source path. Tests emit no inference content. Set `KUNO_TEST_SECOND_WORKER_CONFIG` to a second worker JSON to exercise both cards in the gateway test. To repeat with an idle explicitly assigned GPU: ```sh KUNO_TEST_WORKER_CONFIG=/absolute/path/to/worker.json \ cargo test -p kuno --test worker_live -- --ignored --test-threads=1 ``` Remaining before default activation: compatibility-path enforcement, durable versioned profiles, inference peak measurement, external-pressure classification, automatic system discovery, and independent-GPU request concurrency. Idle profiling remains a later milestone; experimental image execution is described in [image development](../../docs/kuno-images.md). [Bonsai 2 and native Rust engine development](../../docs/kuno-bonsai.md) records the additional engine track and its Ollama model-list/API compatibility contract. Driver/API references: [NVIDIA NVML](https://docs.nvidia.com/deploy/nvml-api/api/group__nvmlDeviceQueries.html), [Linux Xe UAPI](https://github.com/torvalds/linux/blob/master/include/uapi/drm/xe_drm.h), [Ollama running models](https://docs.ollama.com/api/ps). ## Run alongside Ollama From the repository root, with Ollama already listening on `127.0.0.1:11434`: ```sh export KUNO_CONTROL_TOKEN="$(openssl rand -hex 32)" cargo run -p kuno -- serve --config apps/kuno/kuno.example.json ``` Kuno listens on `127.0.0.1:11433`, with its control API on `127.0.0.1:11432`. Point a test client's Ollama URL at port 11433. Existing clients on 11434 continue to reach Ollama directly and therefore do not participate in admission yet. For example, in another terminal: ```sh OLLAMA_HOST=http://127.0.0.1:11433 ollama list curl --fail http://127.0.0.1:11433/api/version ``` The example token command uses OpenSSL only to generate a development credential; Kuno itself does not invoke OpenSSL. Supply a random 32–256-character token using letters, digits, `-`, `_`, or `.`. Do not put the token in a tracked configuration. Only the control listener requires it. The compatibility listener preserves Ollama's client authentication and origin behavior. `KUNO_CONTROL_TOKEN_FILE` can supply the credential instead, including through systemd's `LoadCredential`. Setting both credential sources is rejected. Configuration is JSON, with unknown keys rejected. `check --config FILE` validates it without starting a listener. Upstream and control addresses must be literal loopback socket addresses. All three ports must differ. Set an absolute, dedicated `state_dir` outside the checkout when running elsewhere; relative paths are resolved against the process's working directory. Reuse that same directory on restarts. Each state directory permits one Kuno process at a time. ## What works - Opaque HTTP forwarding to one configured local upstream, including native and compatibility APIs, unknown fields/routes, binary uploads, upstream errors, incremental NDJSON/SSE bodies, and backpressure. No request-body buffering or payload spooling is added. Redirects and possibly executed requests are never followed/replayed by Kuno. - Exact metadata-route exemptions; other requests share one exclusive admission lease through upstream body completion. Catalog changes and unknown routes use the same gate. This is host-wide serialization, not GPU-aware scheduling. - Bounded waiting, queue/header/stream-idle timeouts, and content-free errors. Stream idle timeouts apply when polling upstream; downstream backpressure can retain a lease longer. HTTP protocol upgrades/CONNECT are explicitly unsupported. - A separate bearer-authenticated control surface with status and pause/resume. The compatibility listener does not expose Kuno control handlers. - A fixed, empty in-flight marker written durably before dispatch. Normal upstream completion clears it. On client disconnect (including before response headers), Kuno retains the reservation and discards the remaining response in memory. This drain is bounded by the configured stream idle timeout as a total deadline; it does not replay requests. Stalls, transport failures, or crashes still require recovery when backend completion cannot be established. - A saved Linux boot identity clears an old empty marker after a confirmed host reboot, when the local upstream cannot retain prior work. Restarting only Kuno preserves uncertainty. Legacy markers without a boot record and corrupted state remain subject to explicit recovery. - Read-only systemd discovery that proposes a private upstream port and a Kuno-owned drop-in, while preserving the current loaded-model cap. - Persistent real-use counts and timestamps for catalog-verified model digests. The bounded request observer retains only model identifiers and input presence; it skips nested prompt/image contents without copying them. The route inventory was checked against [Ollama v0.34.2](https://github.com/ollama/ollama/blob/v0.34.2/server/routes.go). Unknown endpoints that start asynchronous work need adapters before their GPU lifetime can be managed. Completing an HTTP response is not proof that such a future asynchronous operation has finished. ## Control API All routes below are available only on the control listener and require `Authorization: Bearer `: | Method | Route | Behavior | | --- | --- | --- | | GET | `/kuno/v1/status` | Version, mode, admission state, and capability flags | | GET | `/kuno/v1/resources` | Native GPU memory observations and Ollama-reported residency | | GET | `/kuno/v1/learning` | Bounded placement-failure evidence and provisional unloaded baselines | | GET | `/kuno/v1/usage` | Aggregate model calls, last-use times, and persistence health | | POST | `/kuno/v1/admission/pause` | Reject new managed work; let active work finish | | POST | `/kuno/v1/admission/resume` | Resume admission unless upstream recovery is required | Pausing is not an immediate GPU reservation. Wait until `active` is false, and ensure `recovery_required` is false, before treating the upstream as quiescent. Loaded models may still occupy GPU memory. The optional native image worker has a separate authenticated generation route and reserves admission through cleanup; see [image API and lifecycle](../../docs/kuno-images.md#api-and-lifecycle). Cancellation closes the upstream HTTP connection, but Kuno cannot yet verify that Ollama has stopped computing. A dropped or failed managed response therefore sets `recovery_required`. Further managed requests receive HTTP 503; metadata and control remain available. `resume` deliberately does not clear this state. For an uninstalled development gateway, recovery is manual: stop Kuno, establish that the upstream has no active work (for a disposable test upstream, stop/restart it), then remove only `upstream-in-flight` from the configured state directory and restart Kuno. Do not reset an actively used Ollama instance merely to clear this marker. Automated runtime reconciliation and safe recovery belong to M2. Changing state directories or deleting a marker without establishing upstream completion defeats the guard. SIGINT/SIGTERM stops new admission and allows up to 30 seconds for draining. A forced interruption retains the marker. The currently active model may remain resident in Ollama; resident models are not the same as active requests. ## Usage tracking Known JSON inference requests with nonempty input and successful upstream response headers count as real calls. Counts describe accepted attempts, including attempts later cancelled or failed while streaming. Metadata and empty-input preload/unload requests do not count. An observer handles arbitrarily large nested input with a small fixed capture bound; it does not buffer the request to parse its settings. Only identities found in `/api/tags` can be persisted. The catalog is cached for up to 60 seconds and invalidated by catalog mutations and opaque operations. Aliases for the same digest share a counter. Unknown identities increment only an unattributed counter; the supplied name is discarded. Future profiling calls must use the internal worker path and must never call the real-use recorder. Seven-day eligibility requires recent real use and the same digest in the current catalog. Expiry does not delete model files or usage history. History is bounded to 4,096 identities, evicting the least recently used entry only at that limit. Atomic writes preserve the previous file after an interrupted replacement. A write failure appears in the usage API; corrupt existing history stops startup instead of silently erasing it. This first observer covers JSON endpoints, not multipart audio, compressed bodies, or future unknown formats. Ambiguous duplicate model fields and incomplete bodies are not attributed. These requests still proxy normally; they do not enroll a model for idle profiling. Pre-existing use before Kuno started is unknown. ## Takeover discovery ```sh cargo run -p kuno -- takeover-plan ``` This command inspects `ollama.service`; `--service NAME.service` selects another systemd service. It prints a JSON proposal and unfinished checks. It does not write a drop-in, restart services, change model limits, or transfer the port. The port probe is advisory; the installer verifies actual binds and rolls back failures. Environment files, unsupported host formats, and ambiguous environment encoding are rejected for manual discovery. Wrapper/process environment and listener ownership are verified with root privileges by the install command. To prepare reviewable installation files without installing them: ```sh cargo run -p kuno -- takeover-bundle --output /tmp/kuno-review ``` The destination must be new. The private bundle contains the current Kuno binary, config, a generated control credential, the proposed Ollama drop-in, and a systemd unit using `DynamicUser`, `StateDirectory`, and `LoadCredential`. Its runtime state path is `/var/lib/kuno`. The unit syntax has been checked with `systemd-analyze` against a staged filesystem. A bundle is a review artifact, not an executable installation plan: `install` repeats discovery and creates fresh protected state. The transaction core journals intent before stopping services, publishes files without overwriting occupied paths, verifies readiness before completion, and rolls back startup failures. Reopening an interrupted journal requires explicit restore before another apply. Restore checks installed content before touching services and refuses later administrator edits or symlink replacements. Complete staging files can be recovered; a torn or edited staging file requires manual review. Existing Ollama overrides are retained, including the one-model setting. Tests simulate service failures/crashes and preserve the original active state. The existing `OLLAMA_MAX_LOADED_MODELS=1` workaround stays effective for the original catalog service. Managed workers override it independently, allowing multiple models when admission and residency checks pass. Three is the planned idle profiling limit, not the production residency limit. ## Privacy and remaining work Kuno writes no inference bodies, URLs, tokens, or backend error text to logs. Its runtime files are the empty instance lock/in-flight marker and aggregate `usage.json` (plus an atomic-write temporary file). Installation bundles additionally contain explicitly generated configuration, credentials, and the executable; transaction journals contain owned-file hashes, never inference-content hashes. The executable disables core dumping on Linux, including pipe-based collectors. Managed units set `MemorySwapMax=0` and disable core files. The removable Ollama drop-in disables request logging and raw stdout/stderr; Kuno supplies classified diagnostics. Host-wide swap/hibernation privacy is not certified. Running the uninstalled development gateway does not alter existing Ollama logging settings. Privileged installation and real client inference through the adopted listener have been tested on this host. Remaining work includes full reboot/restore acceptance and request-format usage coverage. Experimental managed workers provide placement verification, CPU-spill rejection, and model-limit overrides as described above. Idle profiling, a Vashti generation form/chat tool, and full image workflow fidelity validation remain outstanding. Curated Qwen downloads, an experimental native image worker, and Vashti metadata inspection are available as described in [image development](../../docs/kuno-images.md). The status API does not claim a global GPU-placement guarantee. ## Validation ```sh cargo test -p kuno -p kuno-protocol cargo clippy -p kuno -p kuno-protocol --all-targets -- -D warnings cargo fmt -p kuno -p kuno-protocol --check ``` Tests run against disposable loopback HTTP servers and temporary state directories. They exercise streaming, opaque binary uploads, error/redirect fidelity, hop headers, admission, authentication, cancellation, timeouts, restart recovery markers, locking, usage privacy/expiry, and deployment failure recovery without loading a model or changing systemd. # Experimental Qwen image execution Kuno now has a bounded ComfyUI PNG importer, a curated model downloader, and an native image worker embedded in the distributed executable. Vashti includes an Images workspace and generation/editing chat tools, with live previews. GPU configuration and curated model downloads currently use the CLI; an admin collection picker remains future work. This is an experimental beta. ## Attention and cache execution Requests may set `"attention": "fast"` for the Qwen 2.1 CUDA backend. The default is `"standard"`. Fast attention uses the native SageAttention implementation; it can change output pixels and is not supported for Qwen 2512 or Vulkan. Unsupported choices return `image_attention_unsupported`. No Python engine or additional runtime installation is needed. Large Qwen 2.1 edit caches can use bounded pinned host memory. On CUDA, capable workers stage the current and next layer through two GPU buffers, overlapping transfers with computation. Staging counts toward VRAM reservations, and pending transfers are drained before freeing buffers or leaving a request. If the required memory or backend capabilities are unavailable, execution retains the synchronous cache path. Prompts, references, and results still remain transient. ## Exact supported recipe `qwen-image-2512-text-to-image/v1` uses the Qwen Image 2512 FP8 diffusion model, Qwen 2.5 VL 7B scaled FP8 text encoder, and Qwen Image VAE. It supports Euler with the simple scheduler, full denoise, batch size one, and an explicit flow shift. The provided PNG imports as 832×1248, 100 steps, CFG 5, shift 3.1, and seed `684489560344628`. Both direct text and linked primitive-string prompts work. Only active execution-graph nodes affect the import; disconnected LoRA notes are ignored. An active unsupported LoRA or sampler fails explicitly. PNG metadata does not establish the original model hashes. Matching settings also does not imply identical pixels across ComfyUI and the native engine. Seeds are decimal strings in the API to preserve all 64 bits in JavaScript. The current native adapter accepts only nonnegative signed-64-bit seeds; larger imported seeds can be viewed/exported but generation rejects them explicitly. ## Qwen Image 2.1 The additional recipes are `qwen-image-2.1-text-to-image/v1` and `qwen-image-2.1-edit/v1`. They use the INT8 ConvRot diffusion model and Qwen3-VL 8B encoder, plus the dedicated BF16 2.1 VAE. The collection is `qwen-image-2.1-int8` (17.28 GB decimal, Qwen Research license). The 2512 collection remains available independently. The importer follows the active TextEncodeQwenImage21, QwenImage21Cache (auto/default), resolution selector, and ComfySwitchNode branch. Imported seeds, steps, CFG, prompts, and dimensions are preserved. The native 2.1 schedule is automatically resolution-dependent: omit `flow_shift`. Output dimensions use the model's 16-pixel VAE grid without additional rounding to 32. Output is 8-bit RGB/RGBA PNG, including alpha; a ComfyUI 16-bit save setting is reported as an output-format difference. Editing accepts `reference_images`, an ordered list of one to ten base64-encoded PNG, JPEG, or WebP images. Filesystem paths and remote URLs are not accepted. Original references are not embedded in a ComfyUI output PNG: the importer reports how many must be supplied again. Inputs have bounded dimensions and decoded pixel counts; animated images are rejected. JPEG orientation is applied. Each reference is resized with Lanczos to approximately 1024 squared pixels, preserving aspect ratio and rounding dimensions to multiples of 32, matching the supplied ComfyUI encoder's resolution rule. Reference order and alpha are retained. The Vulkan prefix cache uses about 1 MiB per prefix token per condition. The optional CUDA build stores it in FP16 (half the memory), matching the K/V precision supplied to flash attention. Kuno estimates the cache from reference dimensions and a conservative text-token bound. The CUDA policy reserves working space for a segment and allows the engine to stream weights; Vulkan budgets full diffusion weights. It disables caching when the estimate exceeds the available GPU budget. These estimates are not guarantees against other GPU memory pressure. The request body limit is 64 MiB, with at most two image bodies admitted into memory at once. Each encoded reference is limited to 16 MiB and the combined reference payload to 63 MiB. Native execution remains one image at a time. ## Build and models On Linux Mint/Ubuntu, development builds need: ```sh sudo apt install build-essential cmake ninja-build glslc libvulkan-dev spirv-headers glslang-tools ./apps/kuno/build-native-image.sh ./target/release/kuno fetch-models qwen-image-2512-fp8 --directory /absolute/model/store ./target/release/kuno fetch-models qwen-image-2.1-int8 --directory /absolute/model/store ``` Rust is also required for building. Qwen 2.1 INT8 also requires a shader compiler that supports `GL_EXT_integer_dot_product`; Mint's shaderc 2023.8 does not. The build probes this feature and fails with setup instructions rather than silently building an engine that sends those operations to the CPU. A verified local build uses LunarG SDK 1.4.357.1 (archive SHA-256 `4b41e3b30e8aedaa5dac7c136561ab463eb316a25a54e2c6245f2c299ea1fb85`). Set `VULKAN_SDK=/absolute/sdk/1.4.357.1/x86_64` when running the build script; only the compiler and headers are used, without replacing installed drivers. Get the SDK from [LunarG](https://vulkan.lunarg.com/sdk/home). An optional NVIDIA build embeds the CUDA runtime and cuBLAS static libraries, alongside Vulkan. It prefers CUDA when both backends expose the selected physical GPU. Other GPUs retain the Vulkan path. This produces a substantially larger single executable and requires the NVIDIA driver (including `libcuda.so.1`) on the inference host. End users do not need the CUDA development toolkit. ```sh KUNO_IMAGE_CUDA=1 KUNO_CUDA_ROOT=/absolute/cuda/toolkit \ KUNO_CUDA_ARCHITECTURES=86 VULKAN_SDK=/absolute/vulkan/sdk/x86_64 \ ./apps/kuno/build-native-image.sh ``` Architecture 86 targets the RTX 3060; use the architectures required by the intended host, or omit the setting for a native build. CUDA toolkit 13.0 was tested. Obtain development components from [NVIDIA's official redistributions](https://developer.download.nvidia.com/compute/cuda/redist/redistrib_13.0.0.json) and verify their manifest checksums. The toolkits are build inputs, not driver upgrades. Both CUDA and Vulkan shader disk caches are disabled in image workers. Installed binaries need the GPU's working Vulkan driver and the usual native system libraries; they do not require Python or a separate image server. The build pins stable-diffusion.cpp to `88411ef1e0688ff2df1010aeeb5d92b2d8cea2be`, retaining its matching patched GGML. A checked-in compatibility patch fixes quantization-scale tensor name detection and adds bounded INT8 embedding row lookup with inverse ConvRot rotation. It also preserves input scaling while packing INT8 attention projections for the GPU, avoiding the upstream CPU fallback for Qwen's 1/32 attention scale. The build checks the exact patch and runs an independent synthetic numerical test; it refuses unrelated local dependency changes. A runtime kernel-capability probe rejects unsupported INT8 GPUs before model loading with `image_gpu_int8_unsupported`; check the build toolchain and Vulkan driver in that case rather than retrying model downloads. The build is optional; ordinary Kuno builds do not link the image engine. The Qwen 2512 collection pins a Hugging Face repository revision, exact sizes, and SHA-256 hashes for all three artifacts (about 30.1 GB decimal). Downloads resume from private partial files and activate only after checksum verification. Completed public model artifacts are read-only-accessible to the service through its scoped model mount. Existing mismatching files are preserved and rejected. This downloader never accepts model URLs or filesystem paths from PNG metadata. ## Configure the installed service First enable the managed Ollama workers as described in the Kuno README. Create a settings file containing only machine configuration: ```json { "models": "/absolute/model/store", "pci_address": "0000:2d:00.0", "timeout_seconds": 3600, "collections": ["qwen-image-2512-fp8", "qwen-image-2.1-int8"] } ``` Then run the installed binary: ```sh sudo /usr/local/lib/kuno/kuno configure-image --settings /absolute/settings.json ``` This asks for interruption confirmation, verifies all model checksums, and replaces the binary, configuration, and read-only systemd mount transactionally. The model files must be readable by the service. To disable images while retaining Ollama management, use `configure-image --disable`. Upgrades must retain the `native-image` feature while images are configured. The release package builder includes the native engine and its license notices. ## API and lifecycle `POST /kuno/v1/images/generate` is available on the public compatibility listener and the local control listener. Both require the Kuno bearer token. It accepts: ```json { "workflow": "qwen-image-2512-text-to-image/v1", "prompt": "A red wooden boat on a quiet lake.", "negative_prompt": "Blurry, low quality.", "seed": "42", "width": 512, "height": 512, "steps": 20, "cfg": 5.0, "flow_shift": 3.1 } ``` The successful response is `image/png` with `Cache-Control: no-store` and no embedded generation metadata. Vashti owns any eventual output/provenance storage. Model downloads and operational state are separate from inference content. The first scheduler implementation holds the same exclusive admission lease as LLM inference, stops managed Ollama workers on the image GPU, and releases canonical Ollama's resident models. Verified managed workers on other GPUs remain resident. The image runs in a short-lived child process selected by the physical GPU PCI address. Following LLM requests can start workers again. Child cancellation, output limits, and deadlines retain admission through process cleanup; uncertain cleanup requires recovery instead of admitting competing work. The global admission lease still serializes useful LLM/image execution; retaining a model on the other GPU does not yet imply simultaneous inference. The worker reads its recipe through stdin and returns PNG bytes through stdout. It enters its restrictions before any async runtime threads are created. It refuses to read content unless swap is disabled for its cgroup (or the host). Core dumps are disabled. Linux Landlock ABI 3 or newer denies filesystem content writes, file creation, removal, and truncation. Exceptions allow existing GPU device nodes and writes/truncation to this process's ephemeral `/proc/self/task` controls: CUDA names its internal threads through their `comm` entries. This grants no file creation rights; a regression verifies that proc fd aliases cannot bypass disk write denial. All native threads inherit the policy. Shader disk caches are disabled. Backend logs are reduced to fixed error codes and allowlisted engine source locations; prompts and raw backend messages are never logged. Systemd configures `MemorySwapMax=0` for the installed service. Standalone worker development must use equivalent isolation, such as a transient systemd service with swap disabled and an appropriate RAM limit. Do not run unconstrained GPU tests alongside unrelated GPU work. The `max_vram` native budget leaves driver headroom but is not a hard device-wide allocation limit. ## Verification Automated checks cover active-graph import, duplicate JSON keys, PNG checksums, compressed metadata bounds, large seeds, worker cancellation/deadlines/output bounds, authenticated routing, and filesystem write denial. Committed tests use synthetic metadata; the local reference image remains outside the build inputs. On the target Arc Pro B70, a standalone 512×512, four-step synthetic generation completed in about 37 seconds, returned a visually coherent image, used no swap, and released its GPU allocations. This is a smoke result, not a quality or speed comparison with ComfyUI. Full reference execution and cross-engine fidelity must be assessed separately. The RTX 3060 completed the same standalone smoke workload in about 105 seconds. The installed service subsequently passed LLM → native image → LLM handoff, client disconnect cleanup (about 4.2 seconds), and another successful LLM request after cancellation. A separate 100-step request was disconnected after 90 seconds and Kuno was restarted during cleanup: graceful stop/start took about 18 seconds, left no recovery marker or image worker, and subsequent LLM inference succeeded. Final installed-binary verification also passed model listing, generation, and unload from Clark's machine over the original LAN address. Kuno was left active, unpaused, with no resident models or recovery requirement. These are Vulkan results on both cards, not a comparison against an optimized CUDA image runtime. Later B70 image runs produced kernel `xe` job-timeout/engine-reset messages and Vulkan compute failures. The larger 832×1248, 100-step reference run reached sampling but was intentionally stopped before completion. Subsequent failures also reproduced outside the service; changing locked-memory limits and using serialized, smaller Vulkan submissions did not resolve them in that session. Cancellation now requests native cancellation and allows up to 90 seconds for GPU work to finish before a last-resort kill. A forced stop or detected Vulkan device loss requires recovery rather than silently releasing admission. The B70 is also open in Xorg and the desktop app. No device unbind/reset, driver replacement, or reboot was performed. A fresh-boot B70 retest is needed before claiming reliability or attributing the failure to one specific cause. Similar symptoms appear in an [upstream B70/xe report](https://github.com/ggml-org/llama.cpp/issues/24810); that is a diagnostic lead, not proof that this host has the same bug. The current installed experimental image configuration uses the RTX 3060. ### Qwen 2.1 bring-up The 2.1 collection has been downloaded and checksum-verified. Automated tests cover both supplied workflow shapes, ordered references, JPEG/WebP decoding, reference resizing, and INT8 embedding scale/rotation math. A 512×512, four-step generation completed on the RTX 3060 in about 35 seconds. The installed authenticated API repeated that workload in 36.15 seconds while `ornith-1.5:35b` stayed fully resident on the B70 (21,360,363,436 bytes reported by Ollama, context 4096), and the LLM answered again afterward. The output was a visually recognizable boat. These are correctness smoke tests, not a quality or cross-engine performance comparison. The supplied original JPEG also completed the imported 1248×832, 25-step, CFG 4 edit through the installed API in 794.27 seconds on the RTX 3060. The output lengthened the animal and preserved the scene, but had a visible central seam absent from the supplied ComfyUI output. This establishes execution, not quality parity. NVIDIA allocation/mapping warnings occurred during that run; the subsequent build adds the conservative prefix-cache budget described above. That build completed a two-reference request (one 16-bit PNG and one 8-bit PNG, 512×512 output, four steps, CFG 4) in 185.85 seconds with no new kernel messages. The output was visually poor at four steps; this verifies reference handling and bounded execution, not useful multi-image editing quality. Both runs returned metadata-free PNGs with `Cache-Control: no-store`; preview images lived only in RAM. The supplied PNGs contain workflow and generation settings, but no recorded generation duration. The user measured these ComfyUI baselines on this host: | GPU | Cabbage cold | Cabbage warm | Weasel cold | Weasel warm | | --- | ---: | ---: | ---: | ---: | | Arc Pro B70 | 132.06 s | 100.65 s | 78.25 s | 64.48 s | | RTX 3060 | 199.94 s | 193.41 s | 126.14 s | 109.51 s | Cold runs used the original reference seeds. Warm runs incremented each seed by one to force sampling while allowing ComfyUI to reuse preparation. The supplied Intel warm PNGs confirm the seeds and unchanged sampler settings. The user reports visually identical NVIDIA outputs; those files were not supplied for comparison. These timings are user-reported, not extracted from image metadata. Kuno's original Vulkan 794.27-second weasel run was about 6.3 times the ComfyUI cold time on the same RTX 3060. The subsequent CUDA build with FP16 prefix caching completed the same original-seed, 1248×832, 25-step, CFG 4 workflow in **283.19 s** through the restricted Rust image worker. This is about 2.8 times faster than the Vulkan result, but still 2.2 times ComfyUI's cold time. The visible central seam remains, so this does not establish quality parity. The returned RGBA PNG had no metadata and lived only in RAM. No new kernel messages occurred during the run. Initial CUDA validation found that denying writes to thread `comm` entries caused CUDA discovery to fail and silently left Vulkan available. The task-control exception above fixes this without permitting persistent file writes; the final benchmark explicitly verified CUDA selection. A separate 512×512 four-step synthetic edit produced byte-identical pixels with FP32 versus FP16 prefix caches. That narrow numerical check does not establish equivalence for every workflow. The CUDA/Vulkan build is installed on this host. Installed API smoke tests at 512×512 and four steps completed in 12.49 s for Qwen 2.1 (CFG 1) and 78.87 s for Qwen 2512 (CFG 5, shift 3.1). A 35B LLM remained fully resident on the B70 during both 3060 image requests. Disconnecting a subsequent 100-step Qwen 2.1 request after 20 seconds released admission in 1.0 s without forced termination or recovery, and the LLM answered again afterward. These small runs verify service integration, not image quality. Responses were metadata-free PNGs with `Cache-Control: no-store`; no image bytes were saved. The service was left active and unpaused with no loaded models, recovery requirement, or new kernel messages. Clark's machine also reached the public API over the original LAN address. Validation: 118 Rust tests passed (8 hardware tests ignored), CUDA-enabled Clippy passed with warnings denied, and native numerical/backend-selection tests passed. Full ComfyUI performance parity remains unresolved. ### Seam and dimension correction A VAE-only round trip reproduced the central seam without any diffusion sampling. The decoder's allocation fallback selected half-image tiles, which met without overlap at this resolution. Qwen 2.1 now retries with bounded 32-latent-pixel tiles and overlapping context. The complete original-seed weasel edit subsequently finished in 286.25 s with no visible central seam (alpha range 247–255, versus 1–255 before the fix). Images were inspected through a temporary RAM-only preview. Compilation ran during part of this test; its timing is a correctness result, not a clean new performance baseline. The native engine also incorrectly imposed an extra factor of two on Qwen 2.1's 16-pixel VAE grid, although its diffusion projection uses one latent per token. The patch removes that factor, with a native request regression preserving 1184×1776 exactly. The importer and execution validator now accept this size. Reference-image preprocessing retains its separate 32-pixel grid, matching [ComfyUI's Qwen 2.1 encoder](https://github.com/Comfy-Org/ComfyUI/blob/9d3bfc393b3415e146efb8434b32784cc2e69521/comfy_extras/nodes_qwen.py). The complete original-seed, 50-step, CFG 1 cabbage recipe returned exactly 1184×1776 in **354.03 s** on the 3060. The restricted Rust worker returned a metadata-free RGBA PNG (alpha 248–255), inspected in RAM with no visible seam. No compilation or other inference ran alongside it. This remains about 1.77 times the user-reported ComfyUI cold time of 199.94 s on the same card. During a 20-second sampling interval, PCIe receive traffic was only 23–53 MB/s; link saturation does not explain that interval. The earlier four-step run took 50.08 s and served only as a dimension/execution check. An experimental FP16 prefix cache in system RAM produced byte-identical output to the GPU cache in a 512×512, four-step synthetic edit. At 1248×832, four steps, CFG 4, the RAM-cache probe took 63.86 s, with later steps around 9.1 s. This option now has an admission policy that checks MemAvailable and cgroup ancestor memory.max/memory.high headroom, retaining 24 GiB for models and workspace. Unknown accounting disables the RAM option. The full-workflow result with this policy is recorded below. Moving this cache alone does not close the ComfyUI speed gap. Initial profiling used synthetic references and discarded output pixels. On the 3060, a two-step, CFG 4, 1248×832 edit spent about 33 seconds preparing inputs, 68 seconds sampling, and 8 seconds decoding. Per-operation instrumentation attributed 43 seconds to matrix multiplication and 6 seconds to attention; instrumentation synchronizes operations and adds overhead. An uninstrumented four-step B70 run with the prefix cache enabled took 164 seconds, with later steps around 27 seconds. Neither is the supplied 25-step benchmark. A separate 4096×4096 INT8 matrix test took about 13–14 ms through Vulkan on the 3060, versus 1.6–1.9 ms through CUDA cuBLAS (the latter excludes activation packing). This motivates a full CUDA comparison, not an end-to-end speed claim. ### CUDA profiling after correctness fixes A two-step synthetic 1248×832 edit with CFG 4 and an experimental RAM prefix cache attributed 6.47 s to flash attention, 5.64 s to matrix multiplication, and 2.58 s to contiguous tensor copies during sampling. This instrumentation synchronizes each operation; these figures are for locating bottlenecks, not an end-to-end benchmark. Attention at 4,056 query tokens, about 8,130 key tokens, 32 heads and head dimension 128 took about 40 ms per call. Most diffusion weights remained resident for the second step, so repeated full-model transfers alone cannot account for the steady sampling cost. The CUDA FP16 cache now joins current and cached K/V directly in FP16 before flash attention, eliminating an FP16-to-FP32-to-FP16 round trip. Separate 512×512 four-step synthetic edit comparisons returned byte-identical pixels before and after this change, with both GPU and experimental RAM caches. The one-megapixel RAM-cache probe changed from 63.86 s to 63.29 s overall; this is a small result and does not establish ComfyUI performance parity. An isolated, uninstalled attention prototype tested larger query tiles using the existing kernel implementation. A 128-query tile took around 34 ms versus 38–40 ms for the default 64-query tile on a constant-input microbenchmark. Smaller tiles were slower. This prototype has not passed a general numerical comparison and does not modify the pinned ggml checkout or installed engine. The seam/dimension fixes and redundant-conversion removal are installed on this host. Validation passed: 118 Rust tests (8 hardware tests ignored), native numerical/dimension tests, CUDA-enabled Clippy with warnings denied, formatting, and diff checks. Installed API checks took 12.43 s for Qwen 2.1 and 78.32 s for Qwen 2512 at 512×512/four steps; the 35B LLM stayed resident on the B70 during both 3060 requests. Cancellation cleaned up in 1.0 s, the LLM answered afterward, and all models were unloaded. Kuno was active and unpaused, with no recovery requirement or new kernel messages. The full-resolution speed gap is unresolved. Further steady-state profiling reset counters after the second of four synthetic edit steps. The remaining two steps took 16.98 s: attention accounted for 5.09 s, matrix multiplication 3.69 s, contiguous copies 1.43 s, multiplication 0.92 s, and other measured operations about 2.2 s. The remaining time includes graph execution and data movement; it has not been attributed to PCIe specifically. The same physical link is used by ComfyUI, so link width alone cannot establish the cause of the performance difference. A separate SageAttention experiment reduced the isolated attention operation from about 40 ms to 16 ms and a four-step synthetic edit from 63.29 s to 56.86 s. Synthetic oracle comparisons passed the probe thresholds, but quantized attention changes numerical results (including 5.5% relative L2 error in a small high-magnitude case). It remains a diagnostic-only experiment pending full-workflow quality checks. Neither its timing nor the larger attention tile prototype is a claim of end-to-end parity. The Qwen 2.1 fused gate/up projection now uses the backend-supported SwiGLU operation, avoiding two intermediate chunk copies and separate activation and multiply operations. Unsupported backends retain the original operations. 512×512/four-step synthetic edits were byte-identical before and after this change with both GPU and RAM FP16 caches. With fused SwiGLU and the bounded RAM-cache policy, the original-seed 25-step, CFG 4 weasel edit completed in **241.59 s** on the 3060, returning exactly 1248×832 RGBA with no metadata, alpha 247–255 and no visible central seam. The restricted worker confirmed RAM-cache selection. No compilation or other inference ran during this test. This improves on the earlier roughly 283–286 s runs, but remains 1.92 times the user-reported 126.14 s ComfyUI cold result. The combined test does not isolate each optimization's contribution. Validation passed 121 Rust tests (8 hardware tests ignored), native regression tests, CUDA-enabled Clippy with warnings denied, formatting and diff checks. This candidate is installed (binary SHA256 `741256cd2efb378b1b5674b7bfc7b958c9aba41abe153223296ce4b187a66db3`). Installed 512×512/four-step API checks passed for Qwen 2.1 (14.13 s) and Qwen 2512 (79.03 s), retaining a fully GPU-resident 35B LLM on the B70. Cancellation released the image worker in 0.8 s, the LLM answered afterward, and all models were unloaded. The service is active with admission resumed. SageAttention and custom attention tile prototypes remain uninstalled. See [the performance follow-up](kuno-image-performance.md) for the subsequent 187.84-second experimental SageAttention result, the demonstrated cache-transfer scheduling difference, and the next exact optimizations. These newer experiments are not part of the installed binary described above. ### Installed integration checkpoint The installed build now includes the redundant modulation-copy removal, bounded pinned-cache transfer pipeline, and opt-in `attention: "fast"` API setting. SHA256: `b3d073704c093ea88875c070acc6660c54f14237d44746e95ddbfcaa239e6699`. The original weasel edit took 169.25 s with fast attention and confirmed active cache pipelining; the original cabbage took 262.89 s. Both preserved dimensions and passed RAM-only visual seam checks. See the performance follow-up for test conditions and the remaining ComfyUI gap. Standard attention remains the default. Validation passed 122 Rust tests (8 hardware tests ignored), native regressions, CUDA-enabled Clippy, formatting, and diff checks. Multiple-reference fast generation passed in 62.59 s. Cancellation after the standard-attention cache pipeline became active exited in 4.02 s without a forced stop. Further kernel optimizations are deferred. The next work is app integration, pending the user's UI/design direction; this checkpoint does not claim public release readiness. Installed API checks passed: Qwen 2.1 fast attention (512×512/four steps, 11.80 s), Qwen 2512 standard attention (80.14 s), and cancellation cleanup (1.0 s). The 35B LLM stayed fully resident on the B70 during the image requests, answered after cancellation, and was then unloaded. The service is active and unpaused with no recovery requirement or new kernel errors. ### Transient previews (development) The PNG generation endpoint accepts optional `x-kuno-image-preview-id`: 64 random hex characters supplied by the authenticated caller. While that request is active, authenticated `GET /kuno/v1/images/previews/{id}` returns null or the latest `{step, steps, width, height, png_base64}` projection preview, with `Cache-Control: no-store`. The identifier is per request and must never be put in persistent job recipes. The entry is removed after worker cleanup, including cancellation and failure. There is no listing or replay endpoint. An opt-in worker environment flag enables bounded `KIP1` stdout pipe frames: magic, one-byte kind, big-endian u32 payload length, payload. Kind 1 contains step/steps/width/height as four u32 values followed by a metadata-free PNG; kind 2 contains the final PNG. Preview dimensions are at most 512 square, payload at most 1 MiB, final at most 20 MiB. Diagnostics remain on stderr and never contain image frames. Existing callers without previews still receive the original PNG protocol. The engine uses `PREVIEW_PROJ`, not a full VAE decode per sampling step. Native compilation and synthetic transport/UI checks pass. A live RTX 3060 Qwen 2.1 smoke test (512 square, two steps, CFG 1, standard attention) emitted one 32-square RGBA preview per step and the final PNG in 11.05 seconds. The test consumed all image bytes in RAM and saved no images. This is a transport smoke test, not a quality or comparable full-workflow performance benchmark; preview overhead on the normal 50-step workflow and Intel still needs measurement.