From 07bc0d0db944eda8c8998078a9eea5624608ebc5 Mon Sep 17 00:00:00 2001 From: Jason McCartney Date: Tue, 13 Oct 2026 01:01:37 -0700 Subject: [PATCH 1/2] Integrate SparkQwen Atlas with exact per-node fabric settings --- CHANGELOG.md | 2 + THIRD_PARTY_NOTICES.md | 25 + backends/atlas-sparkqwen/README.md | 132 +++++ backends/atlas-sparkqwen/install.sh | 95 ++++ backends/atlas-sparkqwen/pins.json | 29 ++ backends/atlas-sparkqwen/verify-pins.mjs | 112 ++++ backends/catalog.json | 70 +++ docs/clusters.md | 2 + docs/recipes.md | 39 ++ package.json | 6 +- recipes/index.json | 47 ++ ...-dgx-spark-2x-qwen38-flash-next-atlas.json | 398 +++++++++++++++ test/atlas-sparkqwen-recipe.test.mjs | 480 ++++++++++++++++++ 13 files changed, 1434 insertions(+), 3 deletions(-) create mode 100644 backends/atlas-sparkqwen/README.md create mode 100755 backends/atlas-sparkqwen/install.sh create mode 100644 backends/atlas-sparkqwen/pins.json create mode 100755 backends/atlas-sparkqwen/verify-pins.mjs create mode 100644 recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json create mode 100644 test/atlas-sparkqwen-recipe.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 91d88ac..0ee1adf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to LLooM will be documented in this file. The format follows ### Added +- Atlas SparkQwen RC16 two-Spark recipe (`linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas`, profile `8x262k`) with a fail-closed pin gate and a digest-only image installer, and a per-node `${fabricHca}` template variable (node `fabricHca`, else derived from `fabricInterface`, else `rocep1s0f0`) for distributed recipes. The lane stays blocked until the GHCR image digest is pinned. + - LLooM Hear CPU audio analysis, with structured estimates, optional dashboard images, and opt-in upstream interpretation. File and URL inputs require operator configuration. - Four standalone NVIDIA Spark audio recipes (Qwen3-TTS 1.7B CustomVoice, Base voice cloning and VoiceDesign, and Whisper large-v3-turbo). Each runs one pinned, integrity-checked checkpoint in its own container from a locally built `spark-audio` image that replaces a host-local prebuilt image. diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 778c0ba..55d48e3 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -186,3 +186,28 @@ The model `nvidia/GLM-5.3-Flash-NVFP4` (MIT per SparkGLM's licensing notes) and the DFlash2 drafter `incoai/GLM-5.3-Flash-DFlash2` (**CC BY-NC-ND 4.0, non-commercial**) are downloaded from their publishers under their own terms. See the [backend README](backends/atlas-sparkglm/README.md#credits-and-licenses). + +## Atlas SparkQwen engine image (not included) + +The optional Atlas SparkQwen lane in `backends/atlas-sparkqwen` and +`recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json` pulls the +container image `ghcr.io/enntity/atlas-sparkqwen` built from +[Enntity/sparkqwen](https://github.com/Enntity/sparkqwen) at +`85325e5104b2a6864e0cf31ec470a8c3327d4de4` (install tree `656234c336af`). +LLooM starts that image as a separate program; no engine source or binary is +included in the LLooM npm package, and LLooM's files for the lane remain MIT. + +- Engine: [Atlas](https://github.com/Atlas-Inf/atlas) (Atlas-Inf) with + SparkGLM's upstream series and SparkQwen's Qwen3.8-Flash-Next work on the + [Enntity/atlas](https://github.com/Enntity/atlas) fork, at + [`1c43247d93ca050f02cb67b0603e88edfe3d92a5`](https://github.com/Enntity/atlas/tree/1c43247d93ca050f02cb67b0603e88edfe3d92a5) + (recorded in the image's `/opt/atlas/source-manifest.json`) +- License: AGPL-3.0-only. The corresponding source is Enntity/sparkqwen at the + revision above plus that Enntity/atlas commit; SparkQwen's `NOTICE` and + `docs/LICENSING.md` list what the image build fetches. The image ships + CUTLASS's license and the Rust crate notices under `/opt/atlas/notices/`; + the engine's license is at `/LICENSE`. + +The model `nvidia/Qwen3.8-Flash-Next-NVFP4` (NVIDIA Open Model License per its +model card) is downloaded from its publisher under its own terms. See the +[backend README](backends/atlas-sparkqwen/README.md#credits-and-licenses). diff --git a/backends/atlas-sparkqwen/README.md b/backends/atlas-sparkqwen/README.md new file mode 100644 index 0000000..bc5a886 --- /dev/null +++ b/backends/atlas-sparkqwen/README.md @@ -0,0 +1,132 @@ +# Atlas SparkQwen under LLooM + +LLooM-managed two-node Atlas SparkQwen RC16 lane for a directly connected pair +of NVIDIA DGX Spark systems, serving `nvidia/Qwen3.8-Flash-Next-NVFP4` as +published (no overlay conversion, no separate drafter: speculation uses the +checkpoint's own MTP head). This directory is **MIT orchestration only**: no +Atlas engine source is committed here. The engine image is built from +`Enntity/sparkqwen` revision `85325e5104b2a6864e0cf31ec470a8c3327d4de4` +(install tree `656234c336af`) by that repository's `install/build.sh` and +published as `ghcr.io/enntity/atlas-sparkqwen:656234c336af`. The engine inside +the image is AGPL-3.0-only; see [Credits and licenses](#credits-and-licenses). +SparkQwen qualified RC16 on its own launcher; no live run through LLooM is +recorded yet. + +## Image digest + +`pins.json` addresses `ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e`, the GHCR +manifest digest of tag `656234c336af` (recorded 2026-10-13 with +`docker buildx imagetools inspect`). Both recipe members use the same +reference as `bootstrap.image`. + +## Layout + +- `pins.json` — the single pin manifest: SparkQwen source revision and its + `install/` git tree, the engine commit, the immutable image reference with + the tag kept as metadata, the install-tree label, and the checkpoint revision. +- `verify-pins.mjs` — fail-closed pin gate. Exits non-zero while the manifest + is not `final`, any identity is missing or a DRAFT/TODO placeholder, the + revision, install tree or engine commit is not a 40-character git object id, + the entrypoint or profile is not a plain path under `/opt/atlas`, the tag is not + `ghcr.io/enntity/atlas-sparkqwen:` plus the first 12 characters of the install + tree, `image.reference` is not `ghcr.io/enntity/atlas-sparkqwen@sha256:` plus + 64 lowercase hex characters, an image ID is pinned, or the model leaves + `nvidia/Qwen3.8-Flash-Next-NVFP4@fc694b54fb0174e0913e6adf86691ef85a4ead47`. +- `install.sh` — prepares the image: reuses an already verified local image, + otherwise pulls the immutable digest. It never pulls the mutable tag and + never builds; a failed pull or check is a hard failure. The image must be + arm64 with `io.enntity.sparkqwen.install-tree` equal to the pinned tree and + must ship `/opt/atlas/serve.py`, `/opt/atlas/profiles/8x262k.json` and the + pinned engine commit in `/opt/atlas/source-manifest.json`. `--check-only` + verifies without pulling. For a local build, run `install/build.sh` from + Enntity/sparkqwen at the pinned revision; that produces the tag, not the + digest this recipe addresses. + +## Installation + +```sh +lloom setup --recipe linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas --additive --apply --yes +lloom runtime-start qwen38-flash-next-atlas-cluster +``` + +The recipe is additive: it sets no default model and adds no aliases. Setup +steps, in order: `check-docker`, `verify-atlas-pins`, `download-atlas-model` +(the pinned checkpoint, about 124 GiB, into +`${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4`), `pull-atlas-image` +(`install.sh`), and `prepare-atlas-cuda-cache` (creates +`${installRoot}/atlas-sparkqwen-cuda-cache`, mounted at `/atlas-cuda-cache` so +compiled CUDA kernels survive container recreation). Run setup on both nodes. + +## Container contract + +The image ships its own entrypoint and profiles; LLooM supplies environment and +mounts only, mirroring SparkQwen's reviewed `install/start-node.sh`: host +network and IPC, 32 GiB shm, `--memory 114g`, all GPUs, `/dev/infiniband`, +`IPC_LOCK` and `SYS_NICE`, unlimited memlock, `no-new-privileges`, a 60 s stop +timeout, the checkpoint mounted read-only at the same absolute path, and the +CUDA JIT cache. + +| Variable | Source | Meaning | +| ------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `NODE_RANK` | `${nodeRank}` | `0` on the leader, `1` on the worker | +| `MASTER_ADDR` | `${leaderAddress}` | the leader's direct-fabric address | +| `MASTER_PORT` | `29510` | distributed rendezvous port | +| `FABRIC_INTERFACE` | `${fabricInterface}` | the node's direct-fabric NIC, also `NCCL_SOCKET_IFNAME`/`GLOO_SOCKET_IFNAME` | +| `FABRIC_HCA` | `${fabricHca}` | the node's RoCE device (below); `serve.py` adds the device behind the port's other PCIe half and sets `NCCL_IB_HCA` and `ATLAS_RDMA_RAILS` from both | +| `MODEL_PATH` | `${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4` | the checkpoint | +| `SERVED_MODEL_NAME` | `qwen3.8-flash-next-atlas` | gateway model ID | +| `SPARKQWEN_PROFILE` | `8x262k` | `/opt/atlas/profiles/8x262k.json`: up to eight requests of up to 262,144 tokens from one shared BF16 KV pool (about 3.19M tokens at the profile's GPU memory utilization of 0.88); the image also ships `8x32k` and `4x262k` | +| `SPARKQWEN_FP8_GDN` | `0` | the lossy FP8 Gated DeltaNet opt-in stays off | + +The profile sets every `ATLAS_*` and `NCCL_*` tuning value; `serve.py` drops +ambient `ATLAS_*` variables so the profile wins. To give the KV pool more of a +dedicated pair's memory, add `-e SPARKQWEN_GPU_MEMORY_UTILIZATION=0.80-0.95` to +both members in a local recipe override. + +`${fabricHca}` and `${fabricInterface}` are materialized per node by LLooM from +the node's own config. Because recipes that consume `${fabricHca}` require exact, +node-local device names, LLooM expects `cluster.nodes..fabricHca` (a +comma-separated list of exact local RDMA device names, without spaces or `:port` +suffixes) and `cluster.nodes..fabricInterface` on every participating node; +it never guesses an HCA from the NIC name, since PCI naming and cabling differ by +host. A pair cabled on the second port therefore needs +`fabricHca: "rocep1s0f1"` and `fabricInterface: "enp1s0f1np1"` on both nodes. +Missing or malformed overrides stop recipe materialization with the node and +field named in the error. + +Ports are fixed by the image's `serve.py` and loopback-bound: rank 0 serves +`127.0.0.1:8893` on the leader and rank 1 binds `127.0.0.1:8894` on the worker. +The leader's port is what LLooM health-checks and routes to; the worker is +readiness-checked through its container state. SparkGLM's lane uses the same +ports, so the two lanes cannot run on the same pair at once (neither fits +beside the other anyway). + +## Distributed startup + +The worker starts first (`order` 10, rank 1, `healthStrategy: "container"`, no +warmup); the leader starts second (`order` 20, rank 0), health-checks +`/health`, runs a POST warmup and owns routing. The first start compiles CUDA +kernels into the cache; allow several minutes. + +## Credits and licenses + +LLooM's files in this directory and the recipe are MIT. The engine they start +is not part of LLooM: + +- **Engine.** [Atlas](https://github.com/Atlas-Inf/atlas) (Atlas-Inf) with + SparkGLM's upstream series and SparkQwen's Qwen3.8-Flash-Next work on the + [Enntity/atlas](https://github.com/Enntity/atlas) fork, branch + `sparkqwen/atlas-20261010-rc16`, licensed **AGPL-3.0-only**. It runs from a + separate container image, `ghcr.io/enntity/atlas-sparkqwen`, alongside LLooM + (an aggregate); no engine source or binary is included in LLooM. +- **Corresponding source.** [Enntity/sparkqwen](https://github.com/Enntity/sparkqwen) + at the pinned revision plus the Enntity/atlas commit recorded in the image's + `/opt/atlas/source-manifest.json` + ([`1c43247d`](https://github.com/Enntity/atlas/tree/1c43247d93ca050f02cb67b0603e88edfe3d92a5)). + SparkQwen's `NOTICE` and `docs/LICENSING.md` list everything else the build + fetches; the image ships CUTLASS's license and the Rust crate notices under + `/opt/atlas/notices/` and the engine's license at `/LICENSE`. +- **Model.** Weights are downloaded from their publisher, not distributed by + LLooM. `nvidia/Qwen3.8-Flash-Next-NVFP4` is under the **NVIDIA Open Model + License** per its model card (SparkQwen `docs/LICENSING.md`); read the model + card before use and before redistributing anything derived from it. diff --git a/backends/atlas-sparkqwen/install.sh b/backends/atlas-sparkqwen/install.sh new file mode 100755 index 0000000..5f29ca9 --- /dev/null +++ b/backends/atlas-sparkqwen/install.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# MIT. LLooM orchestration only. +# +# Prepares (or verifies) the Atlas SparkQwen image pinned in pins.json. This +# script contains no Atlas engine source and never builds: the engine image is +# built by install/build.sh in Enntity/sparkqwen and published to GHCR. +# +# The image is addressed by its immutable registry digest (image.reference, +# ghcr.io/...@sha256:...). The mutable tag is never pulled. The pulled image +# must be arm64 and carry io.enntity.sparkqwen.install-tree equal to the pinned +# install tree (a contents check), and ship the serving entrypoint, the profile +# the recipe selects and the pinned engine commit in its source manifest. +# +# An already verified local image is reused; otherwise the digest is pulled. A +# failed pull or a failed check is a hard failure with no fallback. +# +# --check-only verifies the local image without pulling. +# +# Usage: install.sh [--manifest ] [--check-only] +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MANIFEST="${SCRIPT_DIR}/pins.json" +CHECK_ONLY=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --manifest) MANIFEST="${2:-}"; shift 2 ;; + --manifest=*) MANIFEST="${1#*=}"; shift ;; + --check-only) CHECK_ONLY=1; shift ;; + -h|--help) sed -n '2,19p' "${BASH_SOURCE[0]}"; exit 0 ;; + *) echo "install.sh: unexpected argument: $1" >&2; exit 2 ;; + esac +done + +[[ -f "${MANIFEST}" ]] || { echo "install.sh: pin manifest not found: ${MANIFEST}" >&2; exit 2; } + +fail() { echo "atlas-sparkqwen install: $*" >&2; exit 1; } +note() { echo "atlas-sparkqwen install: $*"; } + +# ---- pin gate: fail closed on placeholder pins ----------------------------- +node "${SCRIPT_DIR}/verify-pins.mjs" "${MANIFEST}" \ + || fail "pin manifest ${MANIFEST} is not a final immutable install manifest; record the image digest before installing" + +pin() { node -e 'const m=require(process.argv[1]);let v=m;for(const k of process.argv[2].split(".")){v=v?.[k];}process.stdout.write(String(v??""))' "${MANIFEST}" "$1"; } + +INSTALL_TREE="$(pin source.installTree)" +ENGINE_COMMIT="$(pin engine.commit)" +IMAGE_REFERENCE="$(pin image.reference)" +IMAGE_LABEL="$(pin image.label)" +IMAGE_ARCHITECTURE="$(pin image.architecture)" +ENTRYPOINT_PATH="$(pin image.entrypoint)" +PROFILE_PATH="$(pin image.profilePath)" + +command -v docker >/dev/null 2>&1 || fail "docker is required to prepare and verify the Atlas SparkQwen image" + +verify_image_identity() { + local identity + identity="$(docker image inspect --format "{{.Architecture}} {{index .Config.Labels \"${IMAGE_LABEL}\"}}" "${IMAGE_REFERENCE}" 2>/dev/null)" || { + echo "atlas-sparkqwen install: image ${IMAGE_REFERENCE} is not present locally" >&2 + return 1 + } + [[ "${identity}" == "${IMAGE_ARCHITECTURE} ${INSTALL_TREE}" ]] || { + echo "atlas-sparkqwen install: image ${IMAGE_REFERENCE} has architecture and ${IMAGE_LABEL} '${identity}'; expected '${IMAGE_ARCHITECTURE} ${INSTALL_TREE}'" >&2 + return 1 + } + note "image ${IMAGE_REFERENCE} verified: arch=${IMAGE_ARCHITECTURE} ${IMAGE_LABEL}=${INSTALL_TREE}" +} + +verify_image_contract() { + # Run on every path: a matching label alone does not prove the image ships + # the serving entrypoint, the selected profile and the pinned engine. + docker run --rm --entrypoint bash "${IMAGE_REFERENCE}" -lc \ + "test -f '${ENTRYPOINT_PATH}' && test -f '${PROFILE_PATH}' && grep -q '\"${ENGINE_COMMIT}\"' /opt/atlas/source-manifest.json" \ + || fail "image ${IMAGE_REFERENCE} is missing ${ENTRYPOINT_PATH}, ${PROFILE_PATH} or engine commit ${ENGINE_COMMIT} in /opt/atlas/source-manifest.json" + note "image ${IMAGE_REFERENCE} ships ${ENTRYPOINT_PATH}, ${PROFILE_PATH} and engine ${ENGINE_COMMIT}" +} + +if [[ "${CHECK_ONLY}" == "1" ]]; then + verify_image_identity || fail "installed Atlas SparkQwen image ${IMAGE_REFERENCE} failed verification; --check-only never pulls" + verify_image_contract + exit 0 +fi + +if verify_image_identity 2>/dev/null; then + note "existing image ${IMAGE_REFERENCE} verified; skipping pull" +else + note "pulling ${IMAGE_REFERENCE}" + docker pull "${IMAGE_REFERENCE}" \ + || fail "could not pull the pinned immutable image ${IMAGE_REFERENCE}; refusing to fall back to a mutable tag or a build" + verify_image_identity \ + || fail "pulled image ${IMAGE_REFERENCE} failed the install-tree contents check; fix the digest pin" +fi +verify_image_contract +note "image prepared. LLooM starts runtimes from this image only; nothing is built on the serving path." diff --git a/backends/atlas-sparkqwen/pins.json b/backends/atlas-sparkqwen/pins.json new file mode 100644 index 0000000..cc63497 --- /dev/null +++ b/backends/atlas-sparkqwen/pins.json @@ -0,0 +1,29 @@ +{ + "manifestVersion": 1, + "status": "final", + "note": "Single pin manifest for the Atlas SparkQwen RC16 lane. Source, install tree, tag and model identities are final. image.reference is the GHCR manifest digest of ghcr.io/enntity/atlas-sparkqwen:656234c336af, also used as the recipe's bootstrap image. The normal install path addresses image.reference; the tag is source-build metadata (the first 12 characters of the git tree of install/ at the pinned revision) and the install-tree label is a contents check. A tag or label alone is never the artifact identity.", + "source": { + "repo": "Enntity/sparkqwen", + "revision": "85325e5104b2a6864e0cf31ec470a8c3327d4de4", + "installTree": "656234c336af4d3ba8d274f291d2033c31b998ce", + "cloneUrl": "https://github.com/Enntity/sparkqwen.git", + "buildScript": "install/build.sh" + }, + "engine": { + "repo": "Enntity/atlas", + "branch": "sparkqwen/atlas-20261010-rc16", + "commit": "1c43247d93ca050f02cb67b0603e88edfe3d92a5" + }, + "image": { + "reference": "ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e", + "tag": "ghcr.io/enntity/atlas-sparkqwen:656234c336af", + "label": "io.enntity.sparkqwen.install-tree", + "entrypoint": "/opt/atlas/serve.py", + "profilePath": "/opt/atlas/profiles/8x262k.json", + "architecture": "arm64" + }, + "model": { + "repo": "nvidia/Qwen3.8-Flash-Next-NVFP4", + "revision": "fc694b54fb0174e0913e6adf86691ef85a4ead47" + } +} diff --git a/backends/atlas-sparkqwen/verify-pins.mjs b/backends/atlas-sparkqwen/verify-pins.mjs new file mode 100755 index 0000000..28a4b32 --- /dev/null +++ b/backends/atlas-sparkqwen/verify-pins.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +// MIT. LLooM-side fail-closed pin gate for the Atlas SparkQwen lane. +// +// The single pin manifest is backends/atlas-sparkqwen/pins.json. This checker +// exits non-zero while any required identity is missing, malformed or still a +// DRAFT/TODO placeholder, so such a manifest can never satisfy a setup step, a +// pull, or a start. +// +// The normal install path addresses the image by its immutable registry +// digest: ghcr.io/enntity/atlas-sparkqwen@sha256:.... The tag (the first 12 +// characters of the install/ git tree) is source-build metadata only, and the +// image's io.enntity.sparkqwen.install-tree label is a contents check, not an +// artifact identity. Image IDs are deliberately NOT pinned. +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +export const MANIFEST_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), 'pins.json'); +export const REQUIRED_IMAGE_ARCHITECTURE = 'arm64'; +export const IMAGE_REPOSITORY = 'ghcr.io/enntity/atlas-sparkqwen'; +export const IMAGE_LABEL = 'io.enntity.sparkqwen.install-tree'; +export const IMAGE_REFERENCE_PATTERN = /^ghcr\.io\/enntity\/atlas-sparkqwen@sha256:[a-f0-9]{64}$/; +export const MODEL_REPO = 'nvidia/Qwen3.8-Flash-Next-NVFP4'; +export const MODEL_REVISION = 'fc694b54fb0174e0913e6adf86691ef85a4ead47'; + +const REQUIRED_PINS = [ + ['status', (m) => m.status], + ['source.revision', (m) => m.source?.revision], + ['source.installTree', (m) => m.source?.installTree], + ['engine.commit', (m) => m.engine?.commit], + ['image.reference', (m) => m.image?.reference], + ['image.tag', (m) => m.image?.tag], + ['model.repo', (m) => m.model?.repo], + ['model.revision', (m) => m.model?.revision] +]; + +export function loadPins(manifestPath = MANIFEST_PATH) { + return JSON.parse(fs.readFileSync(manifestPath, 'utf8')); +} + +export function imageTagFor(manifest) { + return `${IMAGE_REPOSITORY}:${String(manifest.source?.installTree ?? '').slice(0, 12)}`; +} + +export function verifyPins(manifest) { + const failures = []; + for (const [name, select] of REQUIRED_PINS) { + const value = select(manifest); + if (typeof value !== 'string' || value.length === 0) failures.push(`${name} is missing`); + else if (/draft|todo/i.test(value)) failures.push(`${name} is a placeholder: ${value}`); + } + if (String(manifest.status).toLowerCase() !== 'final') { + failures.push(`status must be final, got ${manifest.status}`); + } + for (const [name, value] of [ + ['source.revision', manifest.source?.revision], + ['source.installTree', manifest.source?.installTree], + ['engine.commit', manifest.engine?.commit] + ]) { + if (typeof value === 'string' && !/^[a-f0-9]{40}$/.test(value)) { + failures.push(`${name} must be a 40-character lowercase git object id, got ${value}`); + } + } + if (typeof manifest.image?.tag === 'string' && manifest.image.tag !== imageTagFor(manifest)) { + failures.push(`image.tag must be ${imageTagFor(manifest)}, got ${manifest.image.tag}`); + } + const reference = manifest.image?.reference; + if (typeof reference === 'string' && !IMAGE_REFERENCE_PATTERN.test(reference)) { + failures.push(`image.reference must be ${IMAGE_REPOSITORY}@sha256:<64 lowercase hex>, got ${reference}`); + } + if (manifest.image?.label !== IMAGE_LABEL) { + failures.push(`image.label must be ${IMAGE_LABEL}, got ${manifest.image?.label}`); + } + if (manifest.image?.architecture !== REQUIRED_IMAGE_ARCHITECTURE) { + failures.push(`image.architecture must be ${REQUIRED_IMAGE_ARCHITECTURE}, got ${manifest.image?.architecture}`); + } + // install.sh interpolates these into an in-image check command. + for (const name of ['entrypoint', 'profilePath']) { + if (!/^\/opt\/atlas\/[A-Za-z0-9_./-]+$/.test(String(manifest.image?.[name] ?? ''))) { + failures.push(`image.${name} must be a plain path under /opt/atlas, got ${manifest.image?.[name]}`); + } + } + if (Object.hasOwn(manifest.image ?? {}, 'id')) { + failures.push(`image.id must not be pinned: image IDs differ between a pull and a local build`); + } + if (manifest.model?.repo !== MODEL_REPO || manifest.model?.revision !== MODEL_REVISION) { + failures.push(`model must stay on ${MODEL_REPO}@${MODEL_REVISION}`); + } + return failures; +} + +function main() { + const manifestPath = process.argv[2] ? path.resolve(process.argv[2]) : MANIFEST_PATH; + let manifest; + try { + manifest = loadPins(manifestPath); + } catch (error) { + process.stderr.write(`atlas-sparkqwen pins unreadable at ${manifestPath}: ${error.message}\n`); + process.exit(2); + } + const failures = verifyPins(manifest); + if (failures.length > 0) { + process.stderr.write('atlas-sparkqwen pins are not installable:\n'); + for (const failure of failures) process.stderr.write(` - ${failure}\n`); + process.exit(1); + } + process.stdout.write(`atlas-sparkqwen pins verified (${manifest.image.reference})\n`); +} + +if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/backends/catalog.json b/backends/catalog.json index 9b6dbb2..f6d190e 100644 --- a/backends/catalog.json +++ b/backends/catalog.json @@ -1736,6 +1736,76 @@ ], "notes": "Reclamation is unverified for the Atlas engine; confirm abort behaviour on the prepared image before advertising it. The LLooM watchdog drains and evicts on repeated 5xx/timeout regardless." } + }, + { + "id": "docker-atlas-sparkqwen", + "name": "Docker Atlas SparkQwen", + "kind": "openai-compatible-server", + "description": "LLooM-managed Atlas SparkQwen (RC16) containers for Qwen3.8-Flash-Next-NVFP4 on two DGX Sparks. The bundled MIT installer pulls the pinned image from GHCR by its immutable digest, verifies its architecture, install-tree label and in-image contract, and never builds; only the prepared image is started. Tools and reasoning are qualified upstream; structured output is untested with this model and image input is not claimed.", + "platforms": [ + "linux-arm64" + ], + "features": [ + "chat", + "responses", + "streaming", + "usage", + "tools", + "structured-output", + "reasoning", + "mtp", + "speculative-decoding", + "moe", + "long-context", + "cuda", + "blackwell", + "dgx-spark", + "docker", + "nccl", + "distributed" + ], + "commands": [ + "docker", + "bash", + "node" + ], + "setup": [ + { + "id": "check-docker", + "title": "Check Docker", + "action": "check-command", + "command": "docker", + "args": [ + "version" + ] + }, + { + "id": "check-atlas-pins", + "title": "Verify Atlas SparkQwen pins are final", + "action": "command", + "alwaysRun": true, + "command": "node", + "args": [ + "${repoRoot}/backends/atlas-sparkqwen/verify-pins.mjs" + ] + } + ], + "server": { + "protocol": "openai", + "healthPath": "/health", + "chatPath": "/v1/chat/completions" + }, + "cancellation": { + "trigger": "client-disconnect", + "computeReclamation": "unverified", + "modes": [ + "streaming", + "buffered", + "queued", + "generation" + ], + "notes": "Reclamation is unverified for the Atlas engine; confirm abort behaviour on the prepared image before advertising it. The LLooM watchdog drains and evicts on repeated 5xx/timeout regardless." + } } ] } diff --git a/docs/clusters.md b/docs/clusters.md index a941081..3034430 100644 --- a/docs/clusters.md +++ b/docs/clusters.md @@ -68,6 +68,8 @@ Physical membership and model placement are separate. In a three-Spark ring, a m Use private fabric addresses for node-to-node LLooM and raw backend traffic. `backendHost` is deliberately required when a recipe auto-materializes replicas; LLooM binds the generated backend only to that address, never to every interface. +Distributed recipes can template each member's fabric with `${fabricInterface}` (the node's `fabricInterface`) and `${fabricHca}` (the node's `fabricHca`). Recipes that consume `${fabricHca}` require exact, node-local RDMA device names: LLooM expects `fabricHca` (a comma-separated list of unique device names, no spaces or `:port` suffixes) and `fabricInterface` on every participating node, and stops materialization with the node and field named in the error when either is missing or malformed. It never guesses an HCA from the NIC name, because PCI naming and cabling differ by host. + ```json { "server": { "host": "${LLOOM_GATEWAY_HOST}", "port": 8100 }, diff --git a/docs/recipes.md b/docs/recipes.md index e5d86d4..2c44d3b 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -559,3 +559,42 @@ rebuild is required for this LLooM configuration change. Applying NVIDIA Sync discovery refuses ambiguous peer/interface observations. An explicit per-node `fabricHca` selection also preserves that node's configured interface and backend address when refreshing discovery. + +## Atlas SparkQwen RC16 + +`linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas` runs the Atlas SparkQwen +RC16 engine (Enntity/atlas `sparkqwen/atlas-20261010-rc16` @ `1c43247d`) on two +directly connected DGX Sparks, serving the unmodified +`nvidia/Qwen3.8-Flash-Next-NVFP4` checkpoint at revision +`fc694b54fb0174e0913e6adf86691ef85a4ead47` as `qwen3.8-flash-next-atlas`. There +is no overlay conversion and no separate drafter; speculation uses the +checkpoint's own MTP head. The default profile is `8x262k`: up to eight +requests of up to 262,144 tokens from one shared BF16 KV pool. + +```sh +lloom setup --recipe linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas --additive --apply --yes +lloom runtime-start qwen38-flash-next-atlas-cluster +``` + +The recipe is additive (no default model, no aliases, `keepWarm` false). Pins +live in `backends/atlas-sparkqwen/pins.json` (source `Enntity/sparkqwen` +`85325e51`, install tree `656234c336af`). The GHCR manifest digest is pinned in the same manifest. +`node backends/atlas-sparkqwen/verify-pins.mjs`, setup step +`verify-atlas-pins` and `install.sh` reject missing or placeholder identities. The +installer only pulls that digest and verifies the image's arm64 architecture, +`io.enntity.sparkqwen.install-tree` label, entrypoint, profile and engine +commit; it never builds. + +Each member receives `FABRIC_HCA=${fabricHca}` and `FABRIC_SIBLINGS=0`. As with +the SparkGLM recipe, LLooM requires an explicit per-node `fabricHca` (a +comma-separated list of exact local RDMA device names) and `fabricInterface` on +every participating `cluster.nodes` entry; it never guesses an HCA from the NIC +name, because PCI naming and cabling differ by host. Missing or malformed +settings stop recipe materialization with the node and field named in the error. +The image's `serve.py` fixes the API port: rank 0 serves `127.0.0.1:8893` on the +leader (rank 1 binds `127.0.0.1:8894` on the worker), the same ports as the +SparkGLM lane, so the two lanes cannot share a pair. Tools and reasoning are +qualified upstream; structured output is implemented by the engine but untested +with this model, and image input is not claimed. The engine is +**AGPL-3.0-only** and the checkpoint is under the NVIDIA Open Model License; see +the [backend README](../backends/atlas-sparkqwen/README.md#credits-and-licenses). diff --git a/package.json b/package.json index b600a28..d2b8173 100644 --- a/package.json +++ b/package.json @@ -64,16 +64,16 @@ "check": "npm run check:cluster && npm run check:js && npm run check:entity-stagger && npm run check:python && npm run check:shell && npm run check:spark-deploy && npm run check:workers", "check:cluster": "node --check src/cluster.mjs && node --check test/cluster.test.mjs && node --check test/cluster-availability.test.mjs && node --check test/cluster-readiness.test.mjs && node --check src/cluster-deployment.mjs && node --check src/cluster-deployment-cli.mjs && node --check src/ssh-deployment-transport.mjs && node --check test/cluster-deployment.test.mjs && node --check test/cluster-deployment-cli.test.mjs && node --check test/cluster-deployment-e2e.test.mjs", "check:entity-stagger": "node --check scripts/entity-stagger-benchmark.mjs && node --check test/entity-stagger-benchmark.test.mjs", - "check:js": "node --check bin/lloom.mjs && node --check bin/lloom-host.mjs && node --check src/backend-catalog.mjs && node --check src/benchmarks.mjs && node --check src/bootstrap.mjs && node --check src/client-integrations.mjs && node --check src/community-client.mjs && node --check src/community-site.mjs && node --check src/config.mjs && node --check src/dashboard.mjs && node --check src/doctor.mjs && node --check src/host-memory.mjs && node --check src/host-server.mjs && node --check src/init.mjs && node --check src/installer.mjs && node --check src/interchange.mjs && node --check src/machine-profile.mjs && node --check src/managed-environment.mjs && node --check src/model-acquisition.mjs && node --check src/model-intake.mjs && node --check src/model-removal.mjs && node --check src/onboarding.mjs && node --check src/recipe-index.mjs && node --check src/recipe-pack-export.mjs && node --check src/recipe-pack.mjs && node --check src/registry.mjs && node --check src/route-control.mjs && node --check src/recipes.mjs && node --check src/resource-fit.mjs && node --check src/request-scheduler.mjs && node --check src/rate-limit.mjs && node --check src/runtime-manager.mjs && node --check src/runtime-policy.mjs && node --check src/server.mjs && node --check src/deployment-fence.mjs && node --check test/deployment-fence.test.mjs && node --check src/setup.mjs && node --check src/setup-status.mjs && node --check src/process-control.mjs && node --check src/security.mjs && node --check src/standby.mjs && node --check src/tts-catalog.mjs && node --check src/voice-profiles.mjs && node --check src/protocol/text.mjs && node --check src/protocol/reasoning-normalize.mjs && node --check src/protocol/reasoning-effort.mjs && node --check src/protocol/responses.mjs && node --check src/protocol/anthropic.mjs && node --check src/protocol/sse.mjs && node --check src/protocol/stream-anthropic.mjs && node --check src/protocol/stream-responses.mjs && node --check src/protocol/index.mjs && node --check scripts/apply-spark-route-catalog.mjs && node --check scripts/capture-live-metrics.mjs && node --check scripts/merge-live-metrics.mjs && node --check scripts/run-qualitative-chat-benchmark.mjs && node --check scripts/run-local-decision-benchmark.mjs && node --check scripts/check-community-deploy.mjs && node --check scripts/generate-client-configs.mjs && node --check scripts/check-interchange.mjs && node --check scripts/check-package.mjs && node --check scripts/release-lib.mjs && node --check scripts/build-release.mjs && node --check scripts/deploy-spark.mjs && node --check test/audio-generations.test.mjs && node --check test/community-host.test.mjs && node --check test/dashboard-status.test.mjs && node --check test/ds4fv-recipe.test.mjs && node --check test/dspark-hotfixes.test.mjs && node --check test/glm53-exl3-recipe.test.mjs && node --check test/host-memory.test.mjs && node --check test/metrics-persistence.test.mjs && node --check test/model-acquisition.test.mjs && node --check test/model-failover.test.mjs && node --check test/qwen38-vllm-recipe.test.mjs && node --check test/reasoning-normalize.test.mjs && node --check test/reasoning-effort.test.mjs && node --check test/resource-fit.test.mjs && node --check test/route-control.test.mjs && node --check test/spark-route-catalog.test.mjs && node --check test/runtime-watchdog.test.mjs && node --check test/tts-catalog.test.mjs && node --check test/voice-profiles.test.mjs && node --check test/chatterbox-recipe.test.mjs && node --check test/hear-recipe.test.mjs && node --check test/smoke.mjs && node --check src/runtime-supervisor.mjs && node --check test/runtime-supervisor.test.mjs && node --check test/comfyui-media.test.mjs && node --check test/spark-audio-recipes.test.mjs && node --check src/runtime-residency.mjs && node --check test/ltx-pipelines.test.mjs && node --check test/ltx-pipelines-recipe.test.mjs && node --check src/video-contracts.mjs && node --check src/dashboard-scene.mjs && node --check src/dashboard-presence.mjs && node --check src/dashboard-presence-client.mjs && node --check src/first-run.mjs && node --check src/first-run-page.mjs && node --check src/browser-setup.mjs && node --check src/runtime-preferences.mjs && node --check src/installation-jobs.mjs && node --check src/model-installation.mjs && node --check src/dashboard-installation.mjs && node --check src/runtime-memory-safety.mjs && node --check src/dashboard-memory.mjs && node --check src/runtime-memory-usage.mjs && node --check src/config-profiles.mjs && node --check test/config-profiles.test.mjs && node --check src/runtime-policy-config.mjs && node --check test/runtime-policy-config.test.mjs && node --check src/audio-providers.mjs && node --check src/protocol/openrouter-provider.mjs && node --check test/audio-providers.test.mjs && node --check test/openrouter-provider.test.mjs && node --check backends/atlas-sparkglm/verify-pins.mjs && node --check test/atlas-sparkglm-recipe.test.mjs && node --check src/head-preparation.mjs && node --check src/head-transfer.mjs && node --check src/service-control.mjs && node --check src/gateway-relay.mjs && node --check src/client-retarget.mjs && node --check src/head-promotion.mjs && node --check src/head-promotion-transfer.mjs && node --check src/gateway-client.mjs && node --check test/cli-diagnostics.test.mjs && node --check src/service-install.mjs && node --check test/service-install.test.mjs && node --check src/cluster-deployment.mjs && node --check src/cluster-deployment-cli.mjs && node --check src/ssh-deployment-transport.mjs && node --check src/node-release-agent.mjs && node --check src/node-gateway-adapter.mjs && node --check src/node-agent-cli.mjs && node --check test/cluster-deployment.test.mjs && node --check test/cluster-deployment-cli.test.mjs && node --check test/cluster-deployment-e2e.test.mjs && node --check test/node-release-agent.test.mjs && node --check test/node-gateway-adapter.test.mjs && node --check test/node-gateway-adapter-e2e.test.mjs && node --check test/deployment-bundle.test.mjs && node --check test/node-agent-cli.test.mjs && node --check test/ssh-deployment-transport.test.mjs && node --check scripts/build-deployment-bundle.mjs", + "check:js": "node --check bin/lloom.mjs && node --check bin/lloom-host.mjs && node --check src/backend-catalog.mjs && node --check src/benchmarks.mjs && node --check src/bootstrap.mjs && node --check src/client-integrations.mjs && node --check src/community-client.mjs && node --check src/community-site.mjs && node --check src/config.mjs && node --check src/dashboard.mjs && node --check src/doctor.mjs && node --check src/host-memory.mjs && node --check src/host-server.mjs && node --check src/init.mjs && node --check src/installer.mjs && node --check src/interchange.mjs && node --check src/machine-profile.mjs && node --check src/managed-environment.mjs && node --check src/model-acquisition.mjs && node --check src/model-intake.mjs && node --check src/model-removal.mjs && node --check src/onboarding.mjs && node --check src/recipe-index.mjs && node --check src/recipe-pack-export.mjs && node --check src/recipe-pack.mjs && node --check src/registry.mjs && node --check src/route-control.mjs && node --check src/recipes.mjs && node --check src/resource-fit.mjs && node --check src/request-scheduler.mjs && node --check src/rate-limit.mjs && node --check src/runtime-manager.mjs && node --check src/runtime-policy.mjs && node --check src/server.mjs && node --check src/deployment-fence.mjs && node --check test/deployment-fence.test.mjs && node --check src/setup.mjs && node --check src/setup-status.mjs && node --check src/process-control.mjs && node --check src/security.mjs && node --check src/standby.mjs && node --check src/tts-catalog.mjs && node --check src/voice-profiles.mjs && node --check src/protocol/text.mjs && node --check src/protocol/reasoning-normalize.mjs && node --check src/protocol/reasoning-effort.mjs && node --check src/protocol/responses.mjs && node --check src/protocol/anthropic.mjs && node --check src/protocol/sse.mjs && node --check src/protocol/stream-anthropic.mjs && node --check src/protocol/stream-responses.mjs && node --check src/protocol/index.mjs && node --check scripts/apply-spark-route-catalog.mjs && node --check scripts/capture-live-metrics.mjs && node --check scripts/merge-live-metrics.mjs && node --check scripts/run-qualitative-chat-benchmark.mjs && node --check scripts/run-local-decision-benchmark.mjs && node --check scripts/check-community-deploy.mjs && node --check scripts/generate-client-configs.mjs && node --check scripts/check-interchange.mjs && node --check scripts/check-package.mjs && node --check scripts/release-lib.mjs && node --check scripts/build-release.mjs && node --check scripts/deploy-spark.mjs && node --check test/audio-generations.test.mjs && node --check test/community-host.test.mjs && node --check test/dashboard-status.test.mjs && node --check test/ds4fv-recipe.test.mjs && node --check test/dspark-hotfixes.test.mjs && node --check test/glm53-exl3-recipe.test.mjs && node --check test/host-memory.test.mjs && node --check test/metrics-persistence.test.mjs && node --check test/model-acquisition.test.mjs && node --check test/model-failover.test.mjs && node --check test/qwen38-vllm-recipe.test.mjs && node --check test/reasoning-normalize.test.mjs && node --check test/reasoning-effort.test.mjs && node --check test/resource-fit.test.mjs && node --check test/route-control.test.mjs && node --check test/spark-route-catalog.test.mjs && node --check test/runtime-watchdog.test.mjs && node --check test/tts-catalog.test.mjs && node --check test/voice-profiles.test.mjs && node --check test/chatterbox-recipe.test.mjs && node --check test/hear-recipe.test.mjs && node --check test/smoke.mjs && node --check src/runtime-supervisor.mjs && node --check test/runtime-supervisor.test.mjs && node --check test/comfyui-media.test.mjs && node --check test/spark-audio-recipes.test.mjs && node --check src/runtime-residency.mjs && node --check test/ltx-pipelines.test.mjs && node --check test/ltx-pipelines-recipe.test.mjs && node --check src/video-contracts.mjs && node --check src/dashboard-scene.mjs && node --check src/dashboard-presence.mjs && node --check src/dashboard-presence-client.mjs && node --check src/first-run.mjs && node --check src/first-run-page.mjs && node --check src/browser-setup.mjs && node --check src/runtime-preferences.mjs && node --check src/installation-jobs.mjs && node --check src/model-installation.mjs && node --check src/dashboard-installation.mjs && node --check src/runtime-memory-safety.mjs && node --check src/dashboard-memory.mjs && node --check src/runtime-memory-usage.mjs && node --check src/config-profiles.mjs && node --check test/config-profiles.test.mjs && node --check src/runtime-policy-config.mjs && node --check test/runtime-policy-config.test.mjs && node --check src/audio-providers.mjs && node --check src/protocol/openrouter-provider.mjs && node --check test/audio-providers.test.mjs && node --check test/openrouter-provider.test.mjs && node --check backends/atlas-sparkglm/verify-pins.mjs && node --check test/atlas-sparkglm-recipe.test.mjs && node --check backends/atlas-sparkqwen/verify-pins.mjs && node --check test/atlas-sparkqwen-recipe.test.mjs && node --check src/head-preparation.mjs && node --check src/head-transfer.mjs && node --check src/service-control.mjs && node --check src/gateway-relay.mjs && node --check src/client-retarget.mjs && node --check src/head-promotion.mjs && node --check src/head-promotion-transfer.mjs && node --check src/gateway-client.mjs && node --check test/cli-diagnostics.test.mjs && node --check src/service-install.mjs && node --check test/service-install.test.mjs && node --check src/cluster-deployment.mjs && node --check src/cluster-deployment-cli.mjs && node --check src/ssh-deployment-transport.mjs && node --check src/node-release-agent.mjs && node --check src/node-gateway-adapter.mjs && node --check src/node-agent-cli.mjs && node --check test/cluster-deployment.test.mjs && node --check test/cluster-deployment-cli.test.mjs && node --check test/cluster-deployment-e2e.test.mjs && node --check test/node-release-agent.test.mjs && node --check test/node-gateway-adapter.test.mjs && node --check test/node-gateway-adapter-e2e.test.mjs && node --check test/deployment-bundle.test.mjs && node --check test/node-agent-cli.test.mjs && node --check test/ssh-deployment-transport.test.mjs && node --check scripts/build-deployment-bundle.mjs", "check:python": "python3 -m py_compile backends/dspark-vllm/apply-patch-pack.py backends/dspark-vllm/packs/miaai-dsv4flash-d1b76251-defaults/patches/*.py backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/*.py backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/vision_exp/*.py backends/qwen38-vllm/apply-ple-fp8-patch.py backends/qwen38-sglang/*.py backends/glm53-exl3/*.py backends/mlx-audio/lloom_audio_server.py backends/hear/lloom_hear_server.py backends/chatterbox/lloom_chatterbox_server.py patches/apply_mtplx_longctx_fix.py backends/comfyui-media/bridge/*.py backends/comfyui-media/graphs/*.py backends/comfyui-media/build/*.py backends/comfyui-media/install.py backends/comfyui-media/derive_h3_turbo.py backends/qwen-image-diffusers/*.py backends/ace-step-diffusers/*.py backends/ace-step-diffusers/tests/*.py backends/hear/test/test_hear_security.py backends/spark-audio/install.py backends/spark-audio/lloom_audio_cuda_server.py backends/spark-audio/test_lloom_audio_cuda_server.py backends/ltx-pipelines/*.py src/darwin-memory-usage.py", - "check:shell": "bash -n backends/dspark-vllm/entrypoint.sh backends/dspark-vllm/runtime/build-hardened-image.sh backends/dspark-vllm/packs/miaai-dsv4flash-d1b76251-defaults/patches/*.sh backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/apply-runtime.sh backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/*.sh backends/qwen38-vllm/entrypoint.sh backends/qwen38-sglang/entrypoint.sh backends/glm53-exl3/entrypoint.sh backends/mlx-audio/install.sh backends/hear/install.sh backends/chatterbox/install.sh clients/examples/lloom-claude clients/examples/lloom-codex clients/examples/lloom-hermes clients/examples/lloom-opencode clients/examples/lloom-zero scripts/remote-install-spark.sh scripts/remote-stage-spark-worker.sh backends/atlas-sparkglm/install.sh backends/atlas-sparkglm/convert-overlay.sh", + "check:shell": "bash -n backends/dspark-vllm/entrypoint.sh backends/dspark-vllm/runtime/build-hardened-image.sh backends/dspark-vllm/packs/miaai-dsv4flash-d1b76251-defaults/patches/*.sh backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/apply-runtime.sh backends/dspark-vllm/packs/miaai-ds4fv-f5665e8/patches/*.sh backends/qwen38-vllm/entrypoint.sh backends/qwen38-sglang/entrypoint.sh backends/glm53-exl3/entrypoint.sh backends/mlx-audio/install.sh backends/hear/install.sh backends/chatterbox/install.sh clients/examples/lloom-claude clients/examples/lloom-codex clients/examples/lloom-hermes clients/examples/lloom-opencode clients/examples/lloom-zero scripts/remote-install-spark.sh scripts/remote-stage-spark-worker.sh backends/atlas-sparkglm/install.sh backends/atlas-sparkglm/convert-overlay.sh backends/atlas-sparkqwen/install.sh", "check:spark-deploy": "node --check test/spark-deploy.test.mjs && node test/spark-deploy.test.mjs", "generate:clients": "node scripts/generate-client-configs.mjs", "interchange:check": "node scripts/check-interchange.mjs", "community:check": "node scripts/check-community-deploy.mjs", "package:check": "node scripts/check-package.mjs", "test:adoption": "node test/resource-fit.test.mjs && node test/model-acquisition.test.mjs && node test/dashboard-status.test.mjs", - "test:unit": "node --test test/deployment-fence.test.mjs test/cluster-deployment.test.mjs test/cluster-deployment-cli.test.mjs test/cluster-deployment-e2e.test.mjs test/node-release-agent.test.mjs test/node-gateway-adapter.test.mjs test/node-gateway-adapter-e2e.test.mjs test/deployment-bundle.test.mjs test/node-agent-cli.test.mjs test/ssh-deployment-transport.test.mjs && node test/runtime-health-ownership.test.mjs && node test/long-prefill-transport.test.mjs && node test/sparkglm-managed.test.mjs && node test/protocol.test.mjs && node test/protocol-stream.test.mjs && node test/stream-metrics.test.mjs && node test/responses-codex.test.mjs && node test/security.test.mjs && node test/community-host.test.mjs && node test/ds4fv-recipe.test.mjs && node test/dspark-hotfixes.test.mjs && node test/glm53-exl3-recipe.test.mjs && node test/host-memory.test.mjs && node test/config-profiles.test.mjs && node test/runtime-policy.test.mjs && node --test test/runtime-policy-config.test.mjs && node --test test/request-scheduler.test.mjs test/adaptive-priority.test.mjs test/rate-limit.test.mjs test/runtime-queue.test.mjs test/foreground-priority.test.mjs test/federated-priority.test.mjs test/runtime-supervisor.test.mjs && node test/interchange.test.mjs && node test/server-resilience.test.mjs && node --test test/standby.test.mjs && node test/metrics-persistence.test.mjs && node test/model-failover.test.mjs && node test/qwen38-vllm-recipe.test.mjs && node test/reasoning-normalize.test.mjs && node test/reasoning-effort.test.mjs && node test/route-control.test.mjs && node test/spark-route-catalog.test.mjs && node test/chat-lane-canary.test.mjs && node test/runtime-watchdog.test.mjs && node test/tts-catalog.test.mjs && node test/voice-profiles.test.mjs && node test/chatterbox-recipe.test.mjs && node test/hear-recipe.test.mjs && npm run test:adoption && node --test test/performance-sampler.test.mjs && node --test test/audio-generations.test.mjs && node test/video-providers.test.mjs && node --test test/web-functions.test.mjs && node --test test/config-mutation.test.mjs && node --test test/model-maintenance.test.mjs && node --test test/model-maintenance-control.test.mjs && node --test test/route-maintenance-concurrency.test.mjs && node --test test/model-maintenance-integration.test.mjs && npm run test:workers && node test/comfyui-media.test.mjs && node test/qwen-image-diffusers.test.mjs && node test/ace-step-diffusers.test.mjs && node test/runtime-residency.test.mjs && node test/spark-audio-recipes.test.mjs && node --test test/video-contracts.test.mjs && node test/ltx-pipelines-recipe.test.mjs && npm run test:ux && npm run test:memory-safety && node --test test/dashboard-memory.test.mjs test/runtime-memory-usage.test.mjs && node --test test/speech-stream.test.mjs && node --test test/audio-providers.test.mjs && node test/openrouter-provider.test.mjs && node test/atlas-sparkglm-recipe.test.mjs && node --test test/head-preparation.test.mjs test/head-transfer.test.mjs test/service-control.test.mjs && node --test test/gateway-relay.test.mjs test/client-retarget.test.mjs && node --test test/head-profile-acceptance.test.mjs test/head-promotion.test.mjs test/head-promotion-transfer.test.mjs test/head-transfer-acceptance.test.mjs && node --test test/cli-diagnostics.test.mjs && node --test test/service-install.test.mjs", + "test:unit": "node --test test/deployment-fence.test.mjs test/cluster-deployment.test.mjs test/cluster-deployment-cli.test.mjs test/cluster-deployment-e2e.test.mjs test/node-release-agent.test.mjs test/node-gateway-adapter.test.mjs test/node-gateway-adapter-e2e.test.mjs test/deployment-bundle.test.mjs test/node-agent-cli.test.mjs test/ssh-deployment-transport.test.mjs && node test/runtime-health-ownership.test.mjs && node test/long-prefill-transport.test.mjs && node test/sparkglm-managed.test.mjs && node test/protocol.test.mjs && node test/protocol-stream.test.mjs && node test/stream-metrics.test.mjs && node test/responses-codex.test.mjs && node test/security.test.mjs && node test/community-host.test.mjs && node test/ds4fv-recipe.test.mjs && node test/dspark-hotfixes.test.mjs && node test/glm53-exl3-recipe.test.mjs && node test/host-memory.test.mjs && node test/config-profiles.test.mjs && node test/runtime-policy.test.mjs && node --test test/runtime-policy-config.test.mjs && node --test test/request-scheduler.test.mjs test/adaptive-priority.test.mjs test/rate-limit.test.mjs test/runtime-queue.test.mjs test/foreground-priority.test.mjs test/federated-priority.test.mjs test/runtime-supervisor.test.mjs && node test/interchange.test.mjs && node test/server-resilience.test.mjs && node --test test/standby.test.mjs && node test/metrics-persistence.test.mjs && node test/model-failover.test.mjs && node test/qwen38-vllm-recipe.test.mjs && node test/reasoning-normalize.test.mjs && node test/reasoning-effort.test.mjs && node test/route-control.test.mjs && node test/spark-route-catalog.test.mjs && node test/chat-lane-canary.test.mjs && node test/runtime-watchdog.test.mjs && node test/tts-catalog.test.mjs && node test/voice-profiles.test.mjs && node test/chatterbox-recipe.test.mjs && node test/hear-recipe.test.mjs && npm run test:adoption && node --test test/performance-sampler.test.mjs && node --test test/audio-generations.test.mjs && node test/video-providers.test.mjs && node --test test/web-functions.test.mjs && node --test test/config-mutation.test.mjs && node --test test/model-maintenance.test.mjs && node --test test/model-maintenance-control.test.mjs && node --test test/route-maintenance-concurrency.test.mjs && node --test test/model-maintenance-integration.test.mjs && npm run test:workers && node test/comfyui-media.test.mjs && node test/qwen-image-diffusers.test.mjs && node test/ace-step-diffusers.test.mjs && node test/runtime-residency.test.mjs && node test/spark-audio-recipes.test.mjs && node --test test/video-contracts.test.mjs && node test/ltx-pipelines-recipe.test.mjs && npm run test:ux && npm run test:memory-safety && node --test test/dashboard-memory.test.mjs test/runtime-memory-usage.test.mjs && node --test test/speech-stream.test.mjs && node --test test/audio-providers.test.mjs && node test/openrouter-provider.test.mjs && node test/atlas-sparkglm-recipe.test.mjs && node test/atlas-sparkqwen-recipe.test.mjs && node --test test/head-preparation.test.mjs test/head-transfer.test.mjs test/service-control.test.mjs && node --test test/gateway-relay.test.mjs test/client-retarget.test.mjs && node --test test/head-profile-acceptance.test.mjs test/head-promotion.test.mjs test/head-promotion-transfer.test.mjs test/head-transfer-acceptance.test.mjs && node --test test/cli-diagnostics.test.mjs && node --test test/service-install.test.mjs", "test:cluster": "node test/cluster.test.mjs && node --test test/cluster-availability.test.mjs test/cluster-readiness.test.mjs", "test": "npm run test:cluster && npm run test:unit && npm run test:entity-stagger && npm run smoke && npm run community:check", "test:entity-stagger": "node test/entity-stagger-benchmark.test.mjs", diff --git a/recipes/index.json b/recipes/index.json index fec8d63..2d722c6 100644 --- a/recipes/index.json +++ b/recipes/index.json @@ -1435,6 +1435,53 @@ "type": "local", "url": "recipes/linux-nvidia-dgx-spark-2x-glm53-atlas.json" } + }, + { + "id": "linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas", + "path": "linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json", + "currentVersion": 1, + "versions": [ + { + "version": 1, + "path": "linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json", + "status": "current", + "notes": "Atlas SparkQwen RC16 two-node candidate over the private fabric, additive only, setDefault false, no aliases. Portable pins live in backends/atlas-sparkqwen/pins.json; the GHCR image digest is still a TODO, so the pin gate fails closed until it is recorded. Default profile 8x262k." + } + ], + "name": "2x DGX Spark Qwen3.8 Flash Next NVFP4 Atlas (SparkQwen RC16)", + "summary": "Two-node Atlas SparkQwen RC16 candidate serving the unmodified Qwen3.8-Flash-Next-NVFP4 checkpoint (built-in MTP) from a pinned GHCR image: eight requests of up to 262K tokens.", + "tags": [ + "linux", + "nvidia", + "dgx-spark", + "cluster", + "qwen3.8", + "flash-next", + "nvfp4", + "atlas", + "sparkqwen", + "mtp", + "candidate" + ], + "recommendedFor": [ + "Serving Qwen3.8 Flash Next on the Atlas SparkQwen engine across two directly connected DGX Sparks through LLooM" + ], + "capabilities": [ + "chat", + "responses", + "anthropic-messages", + "streaming", + "tools", + "structured-output", + "reasoning", + "mtp", + "speculative-decoding", + "long-context" + ], + "source": { + "type": "local", + "url": "recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json" + } } ] } diff --git a/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json b/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json new file mode 100644 index 0000000..c76c5e3 --- /dev/null +++ b/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json @@ -0,0 +1,398 @@ +{ + "$schema": "https://lloom.dev/schemas/recipe.v1.schema.json", + "schemaVersion": 1, + "profile": "https://lloom.dev/profiles/interchange/v1", + "id": "linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas", + "name": "2x DGX Spark Qwen3.8 Flash Next NVFP4 Atlas (SparkQwen RC16)", + "version": 1, + "summary": "Two directly connected Sparks running the Atlas SparkQwen RC16 engine over the private fabric, serving the unmodified nvidia/Qwen3.8-Flash-Next-NVFP4 checkpoint with its built-in MTP head: up to eight requests of up to 262144 tokens from one shared BF16 KV pool. Additive only: no default model, no aliases.", + "description": "This recipe installs SparkQwen RC16 at the immutable product revision recorded in backends/atlas-sparkqwen/pins.json. LLooM owns orchestration, admission, worker-first startup, readiness, routing and telemetry. The Atlas engine image is built from Enntity/sparkqwen by that repository's install/build.sh and published to GHCR; LLooM commits no engine source. The engine (Atlas-Inf/atlas with SparkGLM's upstream series and SparkQwen's Qwen3.8-Flash-Next work on the Enntity/atlas fork) is AGPL-3.0-only and runs from its own container image. Setup acquires the pinned checkpoint, pulls the pinned image by its immutable manifest digest (the mutable tag is never pulled), and verifies the image's arm64 architecture, io.enntity.sparkqwen.install-tree contents label and in-image contract. The checkpoint is served as published: no overlay conversion and no separate drafter (speculation uses the checkpoint's own MTP head). Tools and reasoning are qualified by SparkQwen; structured output is implemented by the engine but untested with this model, and image input is not claimed.", + "license": { + "id": "MIT", + "name": "Recipe metadata, launcher and orchestration" + }, + "provenance": { + "createdBy": "lloom", + "generatedBy": "manual-curated", + "source": "SparkQwen RC16: Enntity/sparkqwen 85325e5104b2a6864e0cf31ec470a8c3327d4de4 (install tree 656234c336af4d3ba8d274f291d2033c31b998ce), engine Enntity/atlas sparkqwen/atlas-20261010-rc16 @ 1c43247d (AGPL-3.0-only, recorded in the image's /opt/atlas/source-manifest.json). The image is ghcr.io/enntity/atlas-sparkqwen:656234c336af, addressed by its GHCR manifest digest in pins.json. The engine argument vector ships inside the image as /opt/atlas/profiles/8x262k.json, selected with SPARKQWEN_PROFILE=8x262k: eight sequences of up to 262144 tokens, BF16 KV (about 3.19M pool tokens at the profile's 0.88 GPU memory utilization), prefix caching, MTP speculation (up to 7 drafts), content-loop steering. Model nvidia/Qwen3.8-Flash-Next-NVFP4 revision fc694b54fb0174e0913e6adf86691ef85a4ead47." + }, + "links": [ + { + "rel": "upstream", + "href": "https://github.com/Enntity/sparkqwen/tree/85325e5104b2a6864e0cf31ec470a8c3327d4de4" + }, + { + "rel": "engine", + "href": "https://github.com/Enntity/atlas/tree/1c43247d93ca050f02cb67b0603e88edfe3d92a5" + }, + { + "rel": "engine-upstream", + "href": "https://github.com/Atlas-Inf/atlas" + }, + { + "rel": "model", + "href": "https://huggingface.co/nvidia/Qwen3.8-Flash-Next-NVFP4/tree/fc694b54fb0174e0913e6adf86691ef85a4ead47" + } + ], + "keywords": [ + "linux", + "nvidia", + "dgx-spark", + "cluster", + "qwen3.8", + "flash-next", + "nvfp4", + "atlas", + "sparkqwen", + "mtp", + "candidate" + ], + "capabilities": [ + "chat", + "responses", + "anthropic-messages", + "streaming", + "usage", + "tools", + "structured-output", + "reasoning", + "mtp", + "speculative-decoding", + "moe", + "long-context" + ], + "hardware": { + "family": "NVIDIA DGX Spark / GB10", + "testedMachine": "Two directly connected DGX Spark systems", + "notes": [ + "Candidate lane: SparkQwen qualified RC16 on its own launcher (results/2026-10-10-rc16 in Enntity/sparkqwen); no live run through LLooM is recorded yet.", + "TP2 with expert parallelism 2 across the private ConnectX RoCE fabric. MASTER_ADDR, FABRIC_INTERFACE and FABRIC_HCA come from the cluster node config. FABRIC_HCA and NCCL_IB_HCA are the node's explicit, node-local cluster.nodes[ID].fabricHca list (exact RDMA device names, no :port syntax); LLooM requires it on every participating node and never guesses an HCA from the NIC name, and FABRIC_SIBLINGS=0 stops the engine from adding a backchannel. Explicit lists may include both verified rails.", + "The worker is admitted first (rank 1) and readiness-checked through its container state; the leader starts second (rank 0), serves 127.0.0.1:8893 (fixed by the image) and owns routing.", + "Peak container memory is capped at 114 GiB per node with the profile's 4096 MiB load-time OOM guard; nothing else should use the pair's GPU memory while it serves.", + "Licenses: the Atlas engine in the image is AGPL-3.0-only (not included in LLooM). nvidia/Qwen3.8-Flash-Next-NVFP4 is under the NVIDIA Open Model License per its model card; read it before use." + ] + }, + "requirements": { + "platforms": ["linux-arm64"], + "memoryGb": 114, + "diskGb": 140, + "commands": ["docker", "node"], + "accelerators": ["cuda", "nvidia-gpu", "blackwell", "gb10"], + "cluster": { + "nodes": 2, + "provider": "nvidia-sync", + "topology": "direct" + } + }, + "backend": { + "id": "docker-atlas-sparkqwen", + "name": "Pinned Atlas SparkQwen image", + "type": "openai-compatible-server" + }, + "setup": { + "steps": [ + { + "id": "check-docker", + "title": "Check Docker", + "action": "check-command", + "command": "docker", + "args": ["version"] + }, + { + "id": "verify-atlas-pins", + "title": "Verify the Atlas SparkQwen pin manifest is final", + "action": "command", + "alwaysRun": true, + "command": "node", + "args": ["${repoRoot}/backends/atlas-sparkqwen/verify-pins.mjs"] + }, + { + "id": "download-atlas-model", + "title": "Acquire the pinned Qwen3.8 Flash Next NVFP4 checkpoint", + "action": "download-model", + "provider": "huggingface", + "model": "nvidia/Qwen3.8-Flash-Next-NVFP4", + "revision": "fc694b54fb0174e0913e6adf86691ef85a4ead47" + }, + { + "id": "pull-atlas-image", + "title": "Pull or verify the pinned Atlas SparkQwen image on this node", + "action": "command", + "alwaysRun": true, + "command": "bash", + "args": ["${repoRoot}/backends/atlas-sparkqwen/install.sh"] + }, + { + "id": "prepare-atlas-cuda-cache", + "title": "Create the persistent CUDA JIT cache directory", + "action": "command", + "alwaysRun": true, + "command": "mkdir", + "args": ["-p", "${installRoot}/atlas-sparkqwen-cuda-cache"] + } + ] + }, + "models": [ + { + "role": "default", + "name": "Qwen3.8 Flash Next NVFP4 on Atlas SparkQwen", + "model": "nvidia/Qwen3.8-Flash-Next-NVFP4", + "gatewayModel": "qwen3.8-flash-next-atlas", + "upstreamModel": "qwen3.8-flash-next-atlas", + "runtime": "qwen38-flash-next-atlas-cluster", + "backendConfig": "qwen38-flash-next-atlas", + "setDefault": false, + "input": ["text"], + "output": ["text"], + "capabilities": [ + "chat", + "responses", + "anthropic-messages", + "streaming", + "usage", + "tools", + "structured-output", + "reasoning", + "mtp", + "speculative-decoding", + "moe", + "long-context" + ], + "settings": { + "port": 8893, + "baseUrl": "http://127.0.0.1:8893/v1", + "healthUrl": "http://127.0.0.1:8893/health", + "warmup": { + "url": "http://127.0.0.1:8893/v1/chat/completions", + "method": "POST", + "body": { + "model": "qwen3.8-flash-next-atlas", + "messages": [ + { + "role": "user", + "content": "Reply with OK." + } + ], + "max_tokens": 16, + "stream": false + } + }, + "contextWindow": 262144, + "maxOutputTokens": 32768, + "maxActiveRequests": 8, + "maxQueuedRequests": 8, + "queueTimeoutMs": 300000, + "queueRetryAfterSeconds": 5, + "requestStartupWaitMs": 1000, + "startupRetryAfterSeconds": 30, + "healthTimeoutMs": 5000, + "memoryGb": 114, + "priority": 150, + "keepWarm": false, + "startupTimeoutMs": 3600000, + "timeoutMs": 3600000, + "watchdog": { + "enabled": true, + "failureThreshold": 2, + "failureWindowMs": 900000, + "minNoProgressMs": 1800000, + "cooldownMs": 600000, + "drainTimeoutMs": 30000, + "failureStatuses": [500, 502, 504] + }, + "placement": { + "mode": "distributed", + "members": [ + { + "nodeRole": "worker", + "runtime": "qwen38-flash-next-atlas-worker", + "role": "worker", + "order": 10, + "healthStrategy": "container", + "resources": { + "memoryGb": 114 + }, + "runtimeSettings": { + "adapter": "docker", + "management": "managed", + "containerName": "lloom-atlas-sparkqwen-${nodeId}", + "warmup": false, + "bootstrap": { + "adapter": "docker", + "image": "ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e", + "pull": false, + "pullTimeoutMs": 3600000, + "createArgs": [ + "--restart", + "no", + "--network", + "host", + "--ipc", + "host", + "--shm-size", + "32g", + "--memory", + "114g", + "--gpus", + "all", + "--device", + "/dev/infiniband:/dev/infiniband", + "--cap-add", + "IPC_LOCK", + "--cap-add", + "SYS_NICE", + "--ulimit", + "memlock=-1:-1", + "--security-opt", + "no-new-privileges=true", + "--stop-timeout", + "60", + "--mount", + "type=bind,src=${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4,dst=${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4,readonly", + "--mount", + "type=bind,src=${installRoot}/atlas-sparkqwen-cuda-cache,dst=/atlas-cuda-cache", + "-e", + "NODE_RANK=${nodeRank}", + "-e", + "MASTER_ADDR=${leaderAddress}", + "-e", + "MASTER_PORT=29510", + "-e", + "FABRIC_INTERFACE=${fabricInterface}", + "-e", + "FABRIC_HCA=${fabricHca}", + "-e", + "FABRIC_SIBLINGS=0", + "-e", + "MODEL_PATH=${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4", + "-e", + "SERVED_MODEL_NAME=qwen3.8-flash-next-atlas", + "-e", + "SPARKQWEN_PROFILE=8x262k", + "-e", + "SPARKQWEN_FP8_GDN=0", + "-e", + "NCCL_SOCKET_IFNAME=${fabricInterface}", + "-e", + "GLOO_SOCKET_IFNAME=${fabricInterface}", + "-e", + "NCCL_IB_HCA=${fabricHca}", + "-e", + "HF_HUB_OFFLINE=1", + "-e", + "TRANSFORMERS_OFFLINE=1", + "-e", + "CUDA_CACHE_PATH=/atlas-cuda-cache", + "-e", + "CUDA_CACHE_MAXSIZE=4294967296" + ] + } + } + }, + { + "nodeRole": "leader", + "runtime": "qwen38-flash-next-atlas-head", + "role": "head", + "order": 20, + "resources": { + "memoryGb": 114 + }, + "runtimeSettings": { + "adapter": "docker", + "management": "managed", + "containerName": "lloom-atlas-sparkqwen-${nodeId}", + "healthUrl": "http://127.0.0.1:8893/health", + "healthTimeoutMs": 5000, + "warmup": { + "url": "http://127.0.0.1:8893/v1/chat/completions", + "method": "POST", + "body": { + "model": "qwen3.8-flash-next-atlas", + "messages": [ + { + "role": "user", + "content": "Reply with OK." + } + ], + "max_tokens": 16, + "stream": false + } + }, + "bootstrap": { + "adapter": "docker", + "image": "ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e", + "pull": false, + "pullTimeoutMs": 3600000, + "createArgs": [ + "--restart", + "no", + "--network", + "host", + "--ipc", + "host", + "--shm-size", + "32g", + "--memory", + "114g", + "--gpus", + "all", + "--device", + "/dev/infiniband:/dev/infiniband", + "--cap-add", + "IPC_LOCK", + "--cap-add", + "SYS_NICE", + "--ulimit", + "memlock=-1:-1", + "--security-opt", + "no-new-privileges=true", + "--stop-timeout", + "60", + "--mount", + "type=bind,src=${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4,dst=${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4,readonly", + "--mount", + "type=bind,src=${installRoot}/atlas-sparkqwen-cuda-cache,dst=/atlas-cuda-cache", + "-e", + "NODE_RANK=${nodeRank}", + "-e", + "MASTER_ADDR=${leaderAddress}", + "-e", + "MASTER_PORT=29510", + "-e", + "FABRIC_INTERFACE=${fabricInterface}", + "-e", + "FABRIC_HCA=${fabricHca}", + "-e", + "FABRIC_SIBLINGS=0", + "-e", + "MODEL_PATH=${modelRoot}/nvidia--Qwen3.8-Flash-Next-NVFP4", + "-e", + "SERVED_MODEL_NAME=qwen3.8-flash-next-atlas", + "-e", + "SPARKQWEN_PROFILE=8x262k", + "-e", + "SPARKQWEN_FP8_GDN=0", + "-e", + "NCCL_SOCKET_IFNAME=${fabricInterface}", + "-e", + "GLOO_SOCKET_IFNAME=${fabricInterface}", + "-e", + "NCCL_IB_HCA=${fabricHca}", + "-e", + "HF_HUB_OFFLINE=1", + "-e", + "TRANSFORMERS_OFFLINE=1", + "-e", + "CUDA_CACHE_PATH=/atlas-cuda-cache", + "-e", + "CUDA_CACHE_MAXSIZE=4294967296" + ] + } + } + } + ] + } + } + } + ] +} diff --git a/test/atlas-sparkqwen-recipe.test.mjs b/test/atlas-sparkqwen-recipe.test.mjs new file mode 100644 index 0000000..a52539b --- /dev/null +++ b/test/atlas-sparkqwen-recipe.test.mjs @@ -0,0 +1,480 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { getBackend, loadBackendCatalog, planBackend } from '../src/backend-catalog.mjs'; +import { deriveUserConfig } from '../src/init.mjs'; +import { loadRecipeById, planRecipe } from '../src/recipes.mjs'; +import { + IMAGE_REFERENCE_PATTERN, + IMAGE_REPOSITORY, + imageTagFor, + loadPins, + verifyPins +} from '../backends/atlas-sparkqwen/verify-pins.mjs'; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const backendDir = path.join(root, 'backends', 'atlas-sparkqwen'); +const recipeId = 'linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas'; +const recipe = await loadRecipeById(recipeId); +const catalog = await loadBackendCatalog(); +const pins = loadPins(); +const checkpoint = 'nvidia--Qwen3.8-Flash-Next-NVFP4'; + +// ---- backend catalog entry -------------------------------------------------- +const backend = getBackend(catalog, 'docker-atlas-sparkqwen'); +assert(backend, 'docker-atlas-sparkqwen backend must be packaged'); +assert(backend.commands.includes('docker')); +assert.equal(backend.server.protocol, 'openai'); +assert.equal(backend.server.healthPath, '/health'); +assert.equal(backend.server.chatPath, '/v1/chat/completions'); +assert.deepEqual( + backend.setup.map((step) => step.id), + ['check-docker', 'check-atlas-pins'] +); +assert.equal(backend.setup.find((step) => step.id === 'check-atlas-pins').alwaysRun, true); +assert(!backend.features.includes('vision'), 'image input is untested and must not be claimed'); +const plannedBackend = await planBackend(backend, { checkCommands: false }); +assert(plannedBackend.steps.every((step) => !step.command.join(' ').includes('${'))); + +// ---- recipe shape ----------------------------------------------------------- +assert.equal(recipe.id, recipeId); +assert.equal(recipe.version, 1); +assert.equal(recipe.backend.id, 'docker-atlas-sparkqwen'); +assert.equal(recipe.license.id, 'MIT'); +assert.deepEqual(recipe.requirements.cluster, { nodes: 2, provider: 'nvidia-sync', topology: 'direct' }); +assert(recipe.requirements.accelerators.includes('gb10')); +const stepIds = [ + 'check-docker', + 'verify-atlas-pins', + 'download-atlas-model', + 'pull-atlas-image', + 'prepare-atlas-cuda-cache' +]; +assert.deepEqual( + recipe.setup.steps.map((step) => step.id), + stepIds +); +for (const stepId of ['verify-atlas-pins', 'pull-atlas-image', 'prepare-atlas-cuda-cache']) { + assert.equal(recipe.setup.steps.find((step) => step.id === stepId).alwaysRun, true, `${stepId} must always run`); +} +const downloadStep = recipe.setup.steps.find((step) => step.id === 'download-atlas-model'); +assert.equal(downloadStep.provider, 'huggingface'); +assert.equal(downloadStep.model, pins.model.repo); +assert.equal(downloadStep.revision, pins.model.revision); +assert.equal(pins.model.revision, 'fc694b54fb0174e0913e6adf86691ef85a4ead47'); + +const model = recipe.models[0]; +assert.equal(recipe.models.length, 1); +assert.equal(model.model, 'nvidia/Qwen3.8-Flash-Next-NVFP4'); +assert.equal(model.gatewayModel, 'qwen3.8-flash-next-atlas'); +assert.equal(model.upstreamModel, 'qwen3.8-flash-next-atlas'); +assert.equal(model.runtime, 'qwen38-flash-next-atlas-cluster'); +// Additive lane: never take over the default route, never add aliases. +assert.equal(model.setDefault, false); +assert(!Object.hasOwn(model, 'aliases')); +assert.equal(model.settings.keepWarm, false); +assert.deepEqual(model.input, ['text']); +for (const capability of ['tools', 'reasoning', 'streaming', 'structured-output']) { + assert(model.capabilities.includes(capability), `missing capability ${capability}`); +} +assert(!model.capabilities.includes('vision'), 'image input is untested and must not be claimed'); +// serve.py binds rank 0 to 127.0.0.1:8893; the port is not configurable. +assert.equal(model.settings.port, 8893); +assert.equal(model.settings.baseUrl, 'http://127.0.0.1:8893/v1'); +assert.equal(model.settings.healthUrl, 'http://127.0.0.1:8893/health'); +assert.equal(model.settings.contextWindow, 262144); +assert.equal(model.settings.maxActiveRequests, 8); +assert.equal(model.settings.memoryGb, 114); + +// ---- distributed placement -------------------------------------------------- +const members = model.settings.placement.members; +assert.equal(model.settings.placement.mode, 'distributed'); +assert.deepEqual( + members.map((member) => [member.nodeRole, member.role, member.order]), + [ + ['worker', 'worker', 10], + ['leader', 'head', 20] + ] +); +const [worker, leader] = members; +assert.equal(worker.healthStrategy, 'container'); +assert.equal(worker.runtimeSettings.warmup, false); +assert.equal(leader.runtimeSettings.healthUrl, 'http://127.0.0.1:8893/health'); +assert.equal(leader.runtimeSettings.warmup.method, 'POST'); +assert.equal(leader.runtimeSettings.warmup.body.model, 'qwen3.8-flash-next-atlas'); + +const expectedCreateArgs = [ + '--restart no', + '--network host', + '--ipc host', + '--shm-size 32g', + '--memory 114g', + '--gpus all', + '--device /dev/infiniband:/dev/infiniband', + '--cap-add IPC_LOCK', + '--cap-add SYS_NICE', + '--ulimit memlock=-1:-1', + '--security-opt no-new-privileges=true', + '--stop-timeout 60', + `--mount type=bind,src=\${modelRoot}/${checkpoint},dst=\${modelRoot}/${checkpoint},readonly`, + '--mount type=bind,src=${installRoot}/atlas-sparkqwen-cuda-cache,dst=/atlas-cuda-cache', + '-e NODE_RANK=${nodeRank}', + '-e MASTER_ADDR=${leaderAddress}', + '-e MASTER_PORT=29510', + '-e FABRIC_INTERFACE=${fabricInterface}', + '-e FABRIC_HCA=${fabricHca}', + '-e FABRIC_SIBLINGS=0', + `-e MODEL_PATH=\${modelRoot}/${checkpoint}`, + '-e SERVED_MODEL_NAME=qwen3.8-flash-next-atlas', + '-e SPARKQWEN_PROFILE=8x262k', + '-e SPARKQWEN_FP8_GDN=0', + '-e NCCL_SOCKET_IFNAME=${fabricInterface}', + '-e GLOO_SOCKET_IFNAME=${fabricInterface}', + '-e NCCL_IB_HCA=${fabricHca}', + '-e CUDA_CACHE_PATH=/atlas-cuda-cache', + '-e CUDA_CACHE_MAXSIZE=4294967296' +]; +for (const member of members) { + const settings = member.runtimeSettings; + assert.equal(settings.adapter, 'docker'); + assert.equal(settings.management, 'managed'); + assert.equal(settings.containerName, 'lloom-atlas-sparkqwen-${nodeId}'); + assert.equal(settings.bootstrap.image, pins.image.reference); + assert.equal(settings.bootstrap.pull, false, 'only the prepared image is started'); + assert.deepEqual(settings.bootstrap.command ?? [], [], 'use the image entrypoint'); + const rendered = settings.bootstrap.createArgs.join(' '); + for (const expected of expectedCreateArgs) assert(rendered.includes(expected), `missing ${expected}`); + // The profile owns every engine tuning value; serve.py drops ambient ATLAS_*. + // LLooM supplies the exact rails: no guessable default, no hard-coded device. + assert(!/\bATLAS_|rocep1s0f0/.test(rendered)); + assert(!/\b10\.\d+\.\d+\.\d+\b/.test(rendered)); + assert(!/spark0\d/.test(rendered)); +} +assert.deepEqual(worker.runtimeSettings.bootstrap.createArgs, leader.runtimeSettings.bootstrap.createArgs); + +// ---- plan validity ---------------------------------------------------------- +const plan = planRecipe( + recipe, + { models: [], runtimes: {} }, + { + modelRoot: '/models', + variables: { repoRoot: root, backendRoot: '/backend', installRoot: '/install' }, + platform: 'linux', + arch: 'arm64', + backendIds: new Set(['docker-atlas-sparkqwen']), + checkLocalReferences: false + } +); +assert.deepEqual(plan.validationErrors, []); +assert.deepEqual( + plan.steps.map((step) => step.id), + stepIds +); +assert.equal(plan.steps.find((step) => step.id === 'download-atlas-model').destination, `/models/${checkpoint}`); +assert.deepEqual(plan.steps.find((step) => step.id === 'pull-atlas-image').command, [ + 'bash', + `${root}/backends/atlas-sparkqwen/install.sh` +]); +assert.deepEqual(plan.steps.find((step) => step.id === 'prepare-atlas-cuda-cache').command, [ + 'mkdir', + '-p', + '/install/atlas-sparkqwen-cuda-cache' +]); +for (const step of plan.steps.filter((entry) => entry.action === 'command')) { + assert(!step.command.join(' ').includes('${'), `unresolved recipe template in ${step.id}`); +} + +// ---- materialization on the 03/04 pair (TP link on port 1) ----------------- +const pairConfig = (nodes, extra = {}) => ({ + server: { host: '127.0.0.1', port: 8100 }, + security: {}, + defaults: {}, + models: [], + backends: {}, + runtimes: {}, + aliases: {}, + clientCatalog: { modelOrder: [] }, + cluster: { leaderNode: 'ennspark03', nodes }, + ...extra +}); +const materialize = (config) => + deriveUserConfig(config, recipe, { + modelRoot: '/models', + additive: true, + backendVariables: { repoRoot: root, backendRoot: '/backend', installRoot: '/install' } + }); +const envOf = (runtime) => { + const args = runtime.bootstrap.createArgs; + return Object.fromEntries( + args + .flatMap((arg, index) => (args[index - 1] === '-e' ? [arg] : [])) + .map((entry) => [entry.slice(0, entry.indexOf('=')), entry.slice(entry.indexOf('=') + 1)]) + ); +}; + +const pair = materialize( + pairConfig( + { + ennspark03: { backendHost: '10.0.3.1', fabricInterface: 'enp1s0f1np1', fabricHca: 'rocep1s0f1' }, + ennspark04: { + backendHost: '10.0.3.2', + fabricInterface: 'enp1s0f1np1', + fabricHca: 'rocep1s0f1', + labels: { role: 'worker' } + } + }, + { + defaults: { chatModel: 'existing-model' }, + models: [{ id: 'existing-model', advertise: true }], + aliases: { existing: { members: ['existing-model'], advertise: true } }, + clientCatalog: { modelOrder: ['existing-model'] } + } + ) +); +// Additive: the existing default and alias are untouched. +assert.equal(pair.defaults.chatModel, 'existing-model'); +assert.deepEqual(pair.aliases.existing, { members: ['existing-model'], advertise: true }); +assert(pair.clientCatalog.modelOrder.includes('qwen3.8-flash-next-atlas')); +assert.equal(pair.backends['qwen38-flash-next-atlas'].baseUrl, 'http://127.0.0.1:8893/v1'); +const cluster = pair.runtimes['qwen38-flash-next-atlas-cluster']; +assert.equal(cluster.keepWarm, false); +assert.equal(cluster.healthUrl, 'http://127.0.0.1:8893/health'); +assert.deepEqual( + cluster.placement.members.map((member) => [member.node, member.runtime, member.role, member.order]), + [ + ['ennspark04', 'qwen38-flash-next-atlas-worker', 'worker', 10], + ['ennspark03', 'qwen38-flash-next-atlas-head', 'head', 20] + ] +); +const head = pair.runtimes['qwen38-flash-next-atlas-head']; +const tail = pair.runtimes['qwen38-flash-next-atlas-worker']; +assert.equal(head.node, 'ennspark03'); +assert.equal(tail.node, 'ennspark04'); +assert.equal(tail.healthStrategy, 'container'); +for (const [runtime, nodeId, rank] of [ + [head, 'ennspark03', '0'], + [tail, 'ennspark04', '1'] +]) { + assert(!JSON.stringify(runtime).includes('${'), `${nodeId} runtime has an unresolved template`); + assert.equal(runtime.containerName, `lloom-atlas-sparkqwen-${nodeId}`); + const env = envOf(runtime); + assert.equal(env.NODE_RANK, rank); + assert.equal(env.MASTER_ADDR, '10.0.3.1'); + assert.equal(env.FABRIC_INTERFACE, 'enp1s0f1np1'); + assert.equal(env.FABRIC_HCA, 'rocep1s0f1', `${nodeId} must use the explicit per-node RoCE device`); + assert.equal(env.NCCL_IB_HCA, 'rocep1s0f1', `${nodeId} must pass its RoCE device to NCCL`); + assert.equal(env.FABRIC_SIBLINGS, '0', `${nodeId} must forbid implicit sibling rails`); + assert.equal(env.NCCL_SOCKET_IFNAME, 'enp1s0f1np1'); + assert.equal(env.MODEL_PATH, `/models/${checkpoint}`); + const args = runtime.bootstrap.createArgs.join(' '); + assert(args.includes(`src=/models/${checkpoint},dst=/models/${checkpoint},readonly`)); + assert(args.includes('src=/install/atlas-sparkqwen-cuda-cache,dst=/atlas-cuda-cache')); +} + +// LLooM never infers an HCA from the NIC name: each node must declare its own +// exact, node-local set (names and physical ports can differ between nodes). +// Distinct explicit lists stay exactly as configured, and NCCL_IB_HCA mirrors +// the same per-node value. +const perNodeFabric = materialize( + pairConfig({ + ennspark03: { backendHost: '10.0.3.1', fabricInterface: 'enp1s0f1np1', fabricHca: 'roceP2p1s0f1' }, + ennspark04: { backendHost: '10.0.3.2', fabricInterface: 'ens7', fabricHca: 'mlx5_7,mlx5_9', labels: {} } + }) +); +assert.equal(envOf(perNodeFabric.runtimes['qwen38-flash-next-atlas-head']).FABRIC_HCA, 'roceP2p1s0f1'); +assert.equal(envOf(perNodeFabric.runtimes['qwen38-flash-next-atlas-head']).NCCL_IB_HCA, 'roceP2p1s0f1'); +assert.equal(envOf(perNodeFabric.runtimes['qwen38-flash-next-atlas-worker']).FABRIC_HCA, 'mlx5_7,mlx5_9'); +assert.equal(envOf(perNodeFabric.runtimes['qwen38-flash-next-atlas-worker']).NCCL_IB_HCA, 'mlx5_7,mlx5_9'); + +// A node missing a usable fabricHca/fabricInterface override is refused rather +// than silently defaulted; planning must also leave the input untouched. +const strictNodes = { + ennspark03: { backendHost: '10.0.3.1', fabricInterface: 'enp1s0f1np1', fabricHca: 'rocep1s0f1' }, + ennspark04: { backendHost: '10.0.3.2', fabricInterface: 'enp1s0f1np1', fabricHca: 'rocep1s0f1', labels: {} } +}; +for (const invalid of [ + undefined, + '', + 'rocep1s0f1,', + 'rocep1s0f1,rocep1s0f1', + 'rocep1s0f1:1', + 'rocep1s0f1;true', + ' rocep1s0f1', + ['rocep1s0f1'] +]) { + const config = pairConfig(structuredClone(strictNodes)); + config.cluster.nodes.ennspark04.fabricHca = invalid; + const snapshot = structuredClone(config); + assert.throws(() => materialize(config), /cluster\.nodes\[ennspark04\]\.fabricHca/); + assert.deepEqual(config, snapshot, 'planning must not mutate input'); +} +for (const invalid of [undefined, '', 'eth0;true', ['eth0']]) { + const config = pairConfig(structuredClone(strictNodes)); + config.cluster.nodes.ennspark04.fabricInterface = invalid; + assert.throws(() => materialize(config), /cluster\.nodes\[ennspark04\]\.fabricInterface/); +} +// A node with no fabricHca at all (not just an invalid value) is refused too. +const missingFabric = pairConfig(structuredClone(strictNodes)); +delete missingFabric.cluster.nodes.ennspark04.fabricHca; +assert.throws(() => materialize(missingFabric), /cluster\.nodes\[ennspark04\]\.fabricHca/); + +// ---- pins: final identities, pending digest, fail-closed gate --------------- +assert.equal(pins.source.repo, 'Enntity/sparkqwen'); +assert.equal(pins.source.revision, '85325e5104b2a6864e0cf31ec470a8c3327d4de4'); +assert.equal(pins.source.installTree, '656234c336af4d3ba8d274f291d2033c31b998ce'); +assert.equal(pins.engine.commit, '1c43247d93ca050f02cb67b0603e88edfe3d92a5'); +assert.equal(pins.image.tag, 'ghcr.io/enntity/atlas-sparkqwen:656234c336af'); +assert.equal(pins.image.tag, imageTagFor(pins)); +assert.equal(pins.image.label, 'io.enntity.sparkqwen.install-tree'); +assert.equal(pins.image.profilePath, '/opt/atlas/profiles/8x262k.json'); +assert.equal(pins.image.reference.split('@')[0], IMAGE_REPOSITORY); +const digestPending = !IMAGE_REFERENCE_PATTERN.test(pins.image.reference); +const verifyRun = spawnSync('node', [path.join(backendDir, 'verify-pins.mjs')], { encoding: 'utf8' }); +if (digestPending) { + // TODO(image digest): the gate must refuse until the GHCR digest is pinned. + assert.match(pins.image.reference, /TODO/); + assert(verifyPins(pins).some((failure) => failure.startsWith('image.reference'))); + assert.equal(verifyRun.status, 1, 'verify-pins must fail while the digest is a TODO'); + assert.match(verifyRun.stderr, /image\.reference is a placeholder/); +} else { + assert.deepEqual(verifyPins(pins), []); + assert.equal(verifyRun.status, 0, verifyRun.stderr); +} + +// A manifest with a real-looking digest passes; each malformed identity fails. +const finalManifest = structuredClone(pins); +finalManifest.status = 'final'; +finalManifest.image.reference = `${IMAGE_REPOSITORY}@sha256:${'a'.repeat(64)}`; +assert.deepEqual(verifyPins(finalManifest), []); +const variant = (mutate) => { + const manifest = structuredClone(finalManifest); + mutate(manifest); + return verifyPins(manifest); +}; +for (const [label, mutate] of [ + ['draft status', (m) => (m.status = 'draft')], + ['branch as revision', (m) => (m.source.revision = 'main')], + ['short engine commit', (m) => (m.engine.commit = '1c43247d')], + ['shell in profile path', (m) => (m.image.profilePath = "/opt/atlas/x' ; rm -rf / ; '")], + ['short install tree', (m) => (m.source.installTree = m.source.installTree.slice(0, 12))], + ['tag not derived from tree', (m) => (m.image.tag = `${IMAGE_REPOSITORY}:latest`)], + ['tag as reference', (m) => (m.image.reference = m.image.tag)], + ['other repository', (m) => (m.image.reference = `ghcr.io/enntity/other@sha256:${'a'.repeat(64)}`)], + ['uppercase digest', (m) => (m.image.reference = `${IMAGE_REPOSITORY}@sha256:${'A'.repeat(64)}`)], + ['short digest', (m) => (m.image.reference = `${IMAGE_REPOSITORY}@sha256:${'a'.repeat(63)}`)], + ['missing reference', (m) => delete m.image.reference], + ['wrong label', (m) => (m.image.label = 'io.enntity.sparkglm.install-tree')], + ['wrong architecture', (m) => (m.image.architecture = 'amd64')], + ['pinned image id', (m) => (m.image.id = `sha256:${'b'.repeat(64)}`)], + ['floating model revision', (m) => (m.model.revision = 'main')] +]) { + assert(variant(mutate).length > 0, `${label} must be rejected`); +} + +// ---- install.sh ------------------------------------------------------------- +const bashCheck = spawnSync('bash', ['-n', path.join(backendDir, 'install.sh')], { encoding: 'utf8' }); +assert.equal(bashCheck.status, 0, bashCheck.stderr); + +const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'atlas-sparkqwen-')); +const fakeBin = path.join(tmpRoot, 'bin'); +fs.mkdirSync(fakeBin); +const finalManifestPath = path.join(tmpRoot, 'pins.json'); +fs.writeFileSync(finalManifestPath, `${JSON.stringify(finalManifest, null, 2)}\n`); +// A fake Docker CLI keeps the test CPU-only. The image exists unless +// SQ_IMAGE_DIR is set, in which case only `docker pull` creates it. +const dockerLog = path.join(tmpRoot, 'docker.log'); +fs.writeFileSync( + path.join(fakeBin, 'docker'), + `#!/usr/bin/env bash +set -euo pipefail +echo "$*" >> '${dockerLog}' +present() { [[ -z "\${SQ_IMAGE_DIR:-}" || -f "\${SQ_IMAGE_DIR}/present" ]]; } +case "\${1:-}" in + image) + [[ "\${5:-}" == '${finalManifest.image.reference}' ]] || { echo "unexpected image target \${5:-}" >&2; exit 2; } + present || { echo "No such image" >&2; exit 1; } + echo "\${SQ_IDENTITY:-arm64 ${pins.source.installTree}}" ;; + pull) + [[ "\${2:-}" == '${finalManifest.image.reference}' ]] || { echo "refusing mutable pull" >&2; exit 2; } + [[ "\${SQ_PULL_FAIL:-0}" == 1 ]] && { echo "denied" >&2; exit 1; } + [[ -n "\${SQ_IMAGE_DIR:-}" ]] && touch "\${SQ_IMAGE_DIR}/present"; exit 0 ;; + run) + [[ "$*" == *'${finalManifest.image.reference}'* ]] || { echo "run must use the digest" >&2; exit 2; } + [[ "$*" == *'${pins.image.profilePath}'* && "$*" == *'${pins.engine.commit}'* ]] || exit 3 + [[ "\${SQ_MISSING_CONTRACT:-0}" == 1 ]] && exit 1; exit 0 ;; + *) echo "unexpected fake docker invocation: $*" >&2; exit 2 ;; +esac +`, + { mode: 0o755 } +); +const fakeEnv = { ...process.env, PATH: `${fakeBin}:${process.env.PATH}` }; +const runInstaller = (env = {}, extra = [], manifest = finalManifestPath) => { + fs.rmSync(dockerLog, { force: true }); + const result = spawnSync('bash', [path.join(backendDir, 'install.sh'), '--manifest', manifest, ...extra], { + encoding: 'utf8', + env: { ...fakeEnv, ...env } + }); + result.docker = fs.existsSync(dockerLog) ? fs.readFileSync(dockerLog, 'utf8') : ''; + return result; +}; + +// The checked-in manifest is refused before Docker is touched while pending. +const checkedIn = runInstaller({}, [], path.join(backendDir, 'pins.json')); +if (digestPending) { + assert.notEqual(checkedIn.status, 0); + assert.match(checkedIn.stderr, /not a final immutable install manifest/); + assert.equal(checkedIn.docker, '', 'no docker call before the pin gate passes'); +} +const checked = runInstaller({}, ['--check-only']); +assert.equal(checked.status, 0, `${checked.stdout}\n${checked.stderr}`); +assert.match(checked.stdout, new RegExp(`${pins.image.label}=${pins.source.installTree}`)); +assert(!checked.docker.includes('pull'), '--check-only never pulls'); +for (const identity of [`arm64 ${'f'.repeat(40)}`, `amd64 ${pins.source.installTree}`, 'arm64 ']) { + const mismatched = runInstaller({ SQ_IDENTITY: identity }, ['--check-only']); + assert.notEqual(mismatched.status, 0, `must reject image identity ${identity}`); + assert.match(mismatched.stderr, new RegExp(`expected 'arm64 ${pins.source.installTree}'`)); +} +const missingContract = runInstaller({ SQ_MISSING_CONTRACT: '1' }, ['--check-only']); +assert.notEqual(missingContract.status, 0, 'a matching label must not hide a missing contract'); +assert.match(missingContract.stderr, /is missing \/opt\/atlas\/serve\.py/); +const noImage = fs.mkdtempSync(path.join(tmpRoot, 'none-')); +const checkMissing = runInstaller({ SQ_IMAGE_DIR: noImage }, ['--check-only']); +assert.notEqual(checkMissing.status, 0); +assert.match(checkMissing.stderr, /not present locally/); +assert(!fs.existsSync(path.join(noImage, 'present'))); + +const reused = runInstaller(); +assert.equal(reused.status, 0, `${reused.stdout}\n${reused.stderr}`); +assert.match(reused.stdout, /verified; skipping pull/); +assert(!reused.docker.includes('pull')); +const pullDir = fs.mkdtempSync(path.join(tmpRoot, 'pull-')); +const pulled = runInstaller({ SQ_IMAGE_DIR: pullDir }); +assert.equal(pulled.status, 0, `${pulled.stdout}\n${pulled.stderr}`); +assert.match(pulled.stdout, /pulling ghcr\.io\/enntity\/atlas-sparkqwen@sha256:/); +assert.match(pulled.stdout, /image prepared/); +assert(!pulled.docker.includes(pins.image.tag), 'the mutable tag is never addressed'); +const failedPull = runInstaller({ SQ_IMAGE_DIR: noImage, SQ_PULL_FAIL: '1' }); +assert.notEqual(failedPull.status, 0); +assert.match(failedPull.stderr, /refusing to fall back/); +const wrongContents = runInstaller({ SQ_IDENTITY: `arm64 ${'f'.repeat(40)}` }); +assert.notEqual(wrongContents.status, 0); +assert.match(wrongContents.stderr, /failed the install-tree contents check/); +assert.match(runInstaller({}, ['--bogus']).stderr, /unexpected argument/); +fs.rmSync(tmpRoot, { recursive: true, force: true }); + +// ---- docs and index --------------------------------------------------------- +const readme = await fs.promises.readFile(path.join(backendDir, 'README.md'), 'utf8'); +for (const needle of ['AGPL-3.0-only', 'NVIDIA Open Model', '127.0.0.1:8893', '127.0.0.1:8894', 'FABRIC_HCA', 'TODO']) { + assert(readme.includes(needle), `backend README must mention ${needle}`); +} +const index = JSON.parse(await fs.promises.readFile(path.join(root, 'recipes', 'index.json'), 'utf8')); +const entry = index.recipes.find((item) => item.id === recipe.id); +assert(entry, 'the recipe must be listed in recipes/index.json'); +assert.equal(entry.path, `${recipeId}.json`); +assert.equal(entry.currentVersion, recipe.version); +assert.equal(entry.versions.find((version) => version.status === 'current').path, entry.path); + +console.log('Atlas SparkQwen recipe/backend checks passed'); From 23ae432d992f877db88e8976be9276ef3cfe83bd Mon Sep 17 00:00:00 2001 From: Jason McCartney Date: Tue, 13 Oct 2026 01:14:54 -0700 Subject: [PATCH 2/2] Pin published fabric-portable Spark images and validate recipe catalog --- CHANGELOG.md | 2 +- backends/atlas-sparkglm/README.md | 6 +++--- backends/atlas-sparkglm/pins.json | 8 ++++---- backends/atlas-sparkqwen/README.md | 10 +++++----- backends/atlas-sparkqwen/pins.json | 10 +++++----- docs/recipes.md | 10 +++++----- recipes/index.json | 2 +- recipes/linux-nvidia-dgx-spark-2x-glm53-atlas.json | 6 +++--- ...-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json | 8 ++++---- test/atlas-sparkglm-recipe.test.mjs | 12 ++++++------ test/atlas-sparkqwen-recipe.test.mjs | 6 +++--- test/smoke.mjs | 3 ++- 12 files changed, 42 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ee1adf..688fe11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to LLooM will be documented in this file. The format follows ### Added -- Atlas SparkQwen RC16 two-Spark recipe (`linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas`, profile `8x262k`) with a fail-closed pin gate and a digest-only image installer, and a per-node `${fabricHca}` template variable (node `fabricHca`, else derived from `fabricInterface`, else `rocep1s0f0`) for distributed recipes. The lane stays blocked until the GHCR image digest is pinned. +- Atlas SparkQwen RC16 two-Spark recipe (`linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas`, profile `8x262k`) with a fail-closed pin gate and a digest-only image installer, and explicit per-node `${fabricHca}` settings for distributed recipes. Both Atlas recipes pin the published images with exact rail selection; missing settings and ambiguous discovery fail clearly. - LLooM Hear CPU audio analysis, with structured estimates, optional dashboard images, and opt-in upstream interpretation. File and URL inputs require operator configuration. diff --git a/backends/atlas-sparkglm/README.md b/backends/atlas-sparkglm/README.md index 6953e4a..70c2697 100644 --- a/backends/atlas-sparkglm/README.md +++ b/backends/atlas-sparkglm/README.md @@ -3,7 +3,7 @@ LLooM-managed two-node Atlas SparkGLM candidate for a directly connected pair of NVIDIA DGX Spark systems. This directory is **MIT orchestration only**: no Atlas engine source is committed here. The engine image is built from immutable -revision `3c7d67146929f350ed75fcdfd5e5349db843b483` of `Enntity/sparkglm` by +revision `7a67d9bce4b5f408ad67f2a958ef1a8787136fdd` of `Enntity/sparkglm` by that repository's `install/build.sh`. The engine inside the image is AGPL-3.0-only; see [Credits and licenses](#credits-and-licenses). Live hardware and full serving qualification remain pending. @@ -26,7 +26,7 @@ and full serving qualification remain pending. install-tree label is a contents check, not an artifact identity. - `install.sh` — prepares the image. The normal path reuses an already verified local image, otherwise pulls the immutable manifest digest - `ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a` + `ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c` from GHCR. It never pulls the mutable tag, and a failed pinned digest pull is a hard failure with no silent source build. The explicit `--source-build` option instead clones `Enntity/sparkglm` at the pinned revision, checks that @@ -235,7 +235,7 @@ is not part of LLooM: `ghcr.io/enntity/atlas-sparkglm`, alongside LLooM (an aggregate); no engine source or binary is included in LLooM. - **Corresponding source.** [Enntity/sparkglm](https://github.com/Enntity/sparkglm) - at the pinned revision (`3c7d6714`) plus the Enntity/atlas commit recorded + at the pinned revision (`7a67d9bc`) plus the Enntity/atlas commit recorded in the image's `/opt/atlas/source-manifest.json` ([`dfb9c26a`](https://github.com/Enntity/atlas/tree/dfb9c26a95c96faa9f94c44e23343b0f2b4f9db6)). SparkGLM's `NOTICE` and `docs/LICENSING.md` list everything else it fetches. diff --git a/backends/atlas-sparkglm/pins.json b/backends/atlas-sparkglm/pins.json index 5ff1c95..a6069bc 100644 --- a/backends/atlas-sparkglm/pins.json +++ b/backends/atlas-sparkglm/pins.json @@ -4,14 +4,14 @@ "note": "Single pin manifest for the Atlas SparkGLM candidate. Portable source, image, model and converter identities are final for this candidate; live hardware and serving qualification remains pending. The normal install path addresses image.reference, the immutable GHCR manifest digest. The tag is retained as source-build metadata (it derives from the git tree of install/ at the pinned revision) and as the local tag produced by an explicit --source-build; the install-tree label is a contents check. A tag or label alone is never the artifact identity.", "source": { "repo": "Enntity/sparkglm", - "revision": "3c7d67146929f350ed75fcdfd5e5349db843b483", - "installTree": "246a3a948ab98d19e085bfd39797e554f0ee9261", + "revision": "7a67d9bce4b5f408ad67f2a958ef1a8787136fdd", + "installTree": "f95a9af14236a781bb13f57dcc9ea0342f339905", "cloneUrl": "https://github.com/Enntity/sparkglm.git", "buildScript": "install/build.sh" }, "image": { - "reference": "ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a", - "tag": "ghcr.io/enntity/atlas-sparkglm:246a3a948ab9", + "reference": "ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c", + "tag": "ghcr.io/enntity/atlas-sparkglm:f95a9af14236", "label": "io.enntity.sparkglm.install-tree", "entrypoint": "/opt/atlas/serve.py", "profilePath": "/opt/atlas/profiles/4x1m.json", diff --git a/backends/atlas-sparkqwen/README.md b/backends/atlas-sparkqwen/README.md index bc5a886..c8057c5 100644 --- a/backends/atlas-sparkqwen/README.md +++ b/backends/atlas-sparkqwen/README.md @@ -5,17 +5,17 @@ of NVIDIA DGX Spark systems, serving `nvidia/Qwen3.8-Flash-Next-NVFP4` as published (no overlay conversion, no separate drafter: speculation uses the checkpoint's own MTP head). This directory is **MIT orchestration only**: no Atlas engine source is committed here. The engine image is built from -`Enntity/sparkqwen` revision `85325e5104b2a6864e0cf31ec470a8c3327d4de4` -(install tree `656234c336af`) by that repository's `install/build.sh` and -published as `ghcr.io/enntity/atlas-sparkqwen:656234c336af`. The engine inside +`Enntity/sparkqwen` revision `8b28467324893583838871b6a6b6a28d3059e64f` +(install tree `4ccd961ecf09`) by that repository's `install/build.sh` and +published as `ghcr.io/enntity/atlas-sparkqwen:4ccd961ecf09`. The engine inside the image is AGPL-3.0-only; see [Credits and licenses](#credits-and-licenses). SparkQwen qualified RC16 on its own launcher; no live run through LLooM is recorded yet. ## Image digest -`pins.json` addresses `ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e`, the GHCR -manifest digest of tag `656234c336af` (recorded 2026-10-13 with +`pins.json` addresses `ghcr.io/enntity/atlas-sparkqwen@sha256:9408884dc4bf2c84b7dd097d62effc73f58fffc25a1d67db73d0b4e69516a7c0`, the GHCR +manifest digest of tag `4ccd961ecf09` (recorded 2026-10-13 with `docker buildx imagetools inspect`). Both recipe members use the same reference as `bootstrap.image`. diff --git a/backends/atlas-sparkqwen/pins.json b/backends/atlas-sparkqwen/pins.json index cc63497..6107d03 100644 --- a/backends/atlas-sparkqwen/pins.json +++ b/backends/atlas-sparkqwen/pins.json @@ -1,11 +1,11 @@ { "manifestVersion": 1, "status": "final", - "note": "Single pin manifest for the Atlas SparkQwen RC16 lane. Source, install tree, tag and model identities are final. image.reference is the GHCR manifest digest of ghcr.io/enntity/atlas-sparkqwen:656234c336af, also used as the recipe's bootstrap image. The normal install path addresses image.reference; the tag is source-build metadata (the first 12 characters of the git tree of install/ at the pinned revision) and the install-tree label is a contents check. A tag or label alone is never the artifact identity.", + "note": "Single pin manifest for the Atlas SparkQwen RC16 lane. Source, install tree, tag and model identities are final. image.reference is the GHCR manifest digest of ghcr.io/enntity/atlas-sparkqwen:4ccd961ecf09, also used as the recipe's bootstrap image. The normal install path addresses image.reference; the tag is source-build metadata (the first 12 characters of the git tree of install/ at the pinned revision) and the install-tree label is a contents check. A tag or label alone is never the artifact identity.", "source": { "repo": "Enntity/sparkqwen", - "revision": "85325e5104b2a6864e0cf31ec470a8c3327d4de4", - "installTree": "656234c336af4d3ba8d274f291d2033c31b998ce", + "revision": "8b28467324893583838871b6a6b6a28d3059e64f", + "installTree": "4ccd961ecf09d86c9f13087e42592789b5f56182", "cloneUrl": "https://github.com/Enntity/sparkqwen.git", "buildScript": "install/build.sh" }, @@ -15,8 +15,8 @@ "commit": "1c43247d93ca050f02cb67b0603e88edfe3d92a5" }, "image": { - "reference": "ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e", - "tag": "ghcr.io/enntity/atlas-sparkqwen:656234c336af", + "reference": "ghcr.io/enntity/atlas-sparkqwen@sha256:9408884dc4bf2c84b7dd097d62effc73f58fffc25a1d67db73d0b4e69516a7c0", + "tag": "ghcr.io/enntity/atlas-sparkqwen:4ccd961ecf09", "label": "io.enntity.sparkqwen.install-tree", "entrypoint": "/opt/atlas/serve.py", "profilePath": "/opt/atlas/profiles/8x262k.json", diff --git a/docs/recipes.md b/docs/recipes.md index 2c44d3b..9de2af0 100644 --- a/docs/recipes.md +++ b/docs/recipes.md @@ -374,11 +374,11 @@ the git tree of its `install/` directory, the immutable image manifest reference (`image.reference`) with the tag kept as source-build metadata and the install-tree contents label, the `nvidia/GLM-5.3-Flash-NVFP4` and drafter revisions, and the conversion marker contract. The current source pin is product revision -`3c7d67146929f350ed75fcdfd5e5349db843b483` with install tree -`246a3a948ab98d19e085bfd39797e554f0ee9261`, so the normal install path +`7a67d9bce4b5f408ad67f2a958ef1a8787136fdd` with install tree +`f95a9af14236a781bb13f57dcc9ea0342f339905`, so the normal install path addresses the immutable digest -`ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a`. -The tag `ghcr.io/enntity/atlas-sparkglm:246a3a948ab9` is source-build metadata +`ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c`. +The tag `ghcr.io/enntity/atlas-sparkglm:f95a9af14236` is source-build metadata (first 12 characters of the install tree) and is never pulled; a tag alone is not an immutable artifact identity. A later product revision is adopted by changing the revision, the install tree, the derived tag and the pinned digest. @@ -578,7 +578,7 @@ lloom runtime-start qwen38-flash-next-atlas-cluster The recipe is additive (no default model, no aliases, `keepWarm` false). Pins live in `backends/atlas-sparkqwen/pins.json` (source `Enntity/sparkqwen` -`85325e51`, install tree `656234c336af`). The GHCR manifest digest is pinned in the same manifest. +`8b284673`, install tree `4ccd961ecf09`). The GHCR manifest digest is pinned in the same manifest. `node backends/atlas-sparkqwen/verify-pins.mjs`, setup step `verify-atlas-pins` and `install.sh` reject missing or placeholder identities. The installer only pulls that digest and verifies the image's arm64 architecture, diff --git a/recipes/index.json b/recipes/index.json index 2d722c6..2abc480 100644 --- a/recipes/index.json +++ b/recipes/index.json @@ -1445,7 +1445,7 @@ "version": 1, "path": "linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json", "status": "current", - "notes": "Atlas SparkQwen RC16 two-node candidate over the private fabric, additive only, setDefault false, no aliases. Portable pins live in backends/atlas-sparkqwen/pins.json; the GHCR image digest is still a TODO, so the pin gate fails closed until it is recorded. Default profile 8x262k." + "notes": "Atlas SparkQwen RC16 two-node candidate over the private fabric, additive only, setDefault false, no aliases. Portable pins live in backends/atlas-sparkqwen/pins.json; the published image is pinned by its immutable GHCR digest, and invalid or placeholder pins fail closed. Default profile 8x262k." } ], "name": "2x DGX Spark Qwen3.8 Flash Next NVFP4 Atlas (SparkQwen RC16)", diff --git a/recipes/linux-nvidia-dgx-spark-2x-glm53-atlas.json b/recipes/linux-nvidia-dgx-spark-2x-glm53-atlas.json index 0c85476..3ca7bc0 100644 --- a/recipes/linux-nvidia-dgx-spark-2x-glm53-atlas.json +++ b/recipes/linux-nvidia-dgx-spark-2x-glm53-atlas.json @@ -14,7 +14,7 @@ "provenance": { "createdBy": "lloom", "generatedBy": "manual-curated", - "source": "Atlas SparkGLM candidate at product revision 3c7d67146929f350ed75fcdfd5e5349db843b483 (engine Enntity/atlas sparkglm/atlas-20261012-rc4.2-layered @ dfb9c26a, AGPL-3.0-only, recorded in the image's /opt/atlas/source-manifest.json; corresponding source is Enntity/sparkglm at that revision plus that Enntity/atlas commit). The normal install path pulls the immutable GHCR manifest digest ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a; the tag ghcr.io/enntity/atlas-sparkglm:246a3a948ab9 is source-build metadata and is never pulled. Each host verifies the pulled image's io.enntity.sparkglm.install-tree contents label equals install tree 246a3a948ab98d19e085bfd39797e554f0ee9261 and the image is arm64; the label is a contents check, not an artifact identity. An explicit --source-build builds from Enntity/sparkglm install/ at that revision and produces a LOCAL tag for an explicitly overridden local recipe only, and does not satisfy the normal immutable recipe. Model nvidia/GLM-5.3-Flash-NVFP4 revision 423acf37583782c51c142d145aef733d72943d93 requires a once-per-node overlay conversion gated on conversion.complete.json plus verification; the DFlash2 drafter incoai/GLM-5.3-Flash-DFlash2 revision 7d74cdd881ed7e32c31175984a67823127b66cfe (CC BY-NC-ND 4.0, non-commercial) is served from its checkpoint. The engine argument vector ships inside the image as /opt/atlas/profiles/4x1m.json, selected with SPARKGLM_PROFILE=4x1m, not in this recipe. The profile carries 1048576-token contexts, four sequences sharing one physical KV pool (GLM latent in FP8 with per-128 scales, split between the two nodes by SPARKGLM_KV_SHARD=1, qualified at about 2.38M tokens at the recipe's 0.91 GPU memory utilization with the display carveout, which overrides the profile default of 0.88, and shared with the disk prefix cache; the 48 GiB disk prefix-cache tier is enabled beside the shard, split into 24 GiB KV and 24 GiB SSM tiers per node), FP32 SSM state with KDA record rollback, and DFlash2 (gamma 8) on the head rank; disable-tool-grammar is deliberately not set so structured output and tools stay functional." + "source": "Atlas SparkGLM candidate at product revision 7a67d9bce4b5f408ad67f2a958ef1a8787136fdd (engine Enntity/atlas sparkglm/atlas-20261012-rc4.2-layered @ dfb9c26a, AGPL-3.0-only, recorded in the image's /opt/atlas/source-manifest.json; corresponding source is Enntity/sparkglm at that revision plus that Enntity/atlas commit). The normal install path pulls the immutable GHCR manifest digest ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c; the tag ghcr.io/enntity/atlas-sparkglm:f95a9af14236 is source-build metadata and is never pulled. Each host verifies the pulled image's io.enntity.sparkglm.install-tree contents label equals install tree f95a9af14236a781bb13f57dcc9ea0342f339905 and the image is arm64; the label is a contents check, not an artifact identity. An explicit --source-build builds from Enntity/sparkglm install/ at that revision and produces a LOCAL tag for an explicitly overridden local recipe only, and does not satisfy the normal immutable recipe. Model nvidia/GLM-5.3-Flash-NVFP4 revision 423acf37583782c51c142d145aef733d72943d93 requires a once-per-node overlay conversion gated on conversion.complete.json plus verification; the DFlash2 drafter incoai/GLM-5.3-Flash-DFlash2 revision 7d74cdd881ed7e32c31175984a67823127b66cfe (CC BY-NC-ND 4.0, non-commercial) is served from its checkpoint. The engine argument vector ships inside the image as /opt/atlas/profiles/4x1m.json, selected with SPARKGLM_PROFILE=4x1m, not in this recipe. The profile carries 1048576-token contexts, four sequences sharing one physical KV pool (GLM latent in FP8 with per-128 scales, split between the two nodes by SPARKGLM_KV_SHARD=1, qualified at about 2.38M tokens at the recipe's 0.91 GPU memory utilization with the display carveout, which overrides the profile default of 0.88, and shared with the disk prefix cache; the 48 GiB disk prefix-cache tier is enabled beside the shard, split into 24 GiB KV and 24 GiB SSM tiers per node), FP32 SSM state with KDA record rollback, and DFlash2 (gamma 8) on the head rank; disable-tool-grammar is deliberately not set so structured output and tools stay functional." }, "links": [ { @@ -284,7 +284,7 @@ "warmup": false, "bootstrap": { "adapter": "docker", - "image": "ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a", + "image": "ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c", "pull": false, "pullTimeoutMs": 3600000, "createArgs": [ @@ -443,7 +443,7 @@ }, "bootstrap": { "adapter": "docker", - "image": "ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a", + "image": "ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c", "pull": false, "pullTimeoutMs": 3600000, "createArgs": [ diff --git a/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json b/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json index c76c5e3..7036ae6 100644 --- a/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json +++ b/recipes/linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas.json @@ -14,12 +14,12 @@ "provenance": { "createdBy": "lloom", "generatedBy": "manual-curated", - "source": "SparkQwen RC16: Enntity/sparkqwen 85325e5104b2a6864e0cf31ec470a8c3327d4de4 (install tree 656234c336af4d3ba8d274f291d2033c31b998ce), engine Enntity/atlas sparkqwen/atlas-20261010-rc16 @ 1c43247d (AGPL-3.0-only, recorded in the image's /opt/atlas/source-manifest.json). The image is ghcr.io/enntity/atlas-sparkqwen:656234c336af, addressed by its GHCR manifest digest in pins.json. The engine argument vector ships inside the image as /opt/atlas/profiles/8x262k.json, selected with SPARKQWEN_PROFILE=8x262k: eight sequences of up to 262144 tokens, BF16 KV (about 3.19M pool tokens at the profile's 0.88 GPU memory utilization), prefix caching, MTP speculation (up to 7 drafts), content-loop steering. Model nvidia/Qwen3.8-Flash-Next-NVFP4 revision fc694b54fb0174e0913e6adf86691ef85a4ead47." + "source": "SparkQwen RC16: Enntity/sparkqwen 8b28467324893583838871b6a6b6a28d3059e64f (install tree 4ccd961ecf09d86c9f13087e42592789b5f56182), engine Enntity/atlas sparkqwen/atlas-20261010-rc16 @ 1c43247d (AGPL-3.0-only, recorded in the image's /opt/atlas/source-manifest.json). The image is ghcr.io/enntity/atlas-sparkqwen:4ccd961ecf09, addressed by its GHCR manifest digest in pins.json. The engine argument vector ships inside the image as /opt/atlas/profiles/8x262k.json, selected with SPARKQWEN_PROFILE=8x262k: eight sequences of up to 262144 tokens, BF16 KV (about 3.19M pool tokens at the profile's 0.88 GPU memory utilization), prefix caching, MTP speculation (up to 7 drafts), content-loop steering. Model nvidia/Qwen3.8-Flash-Next-NVFP4 revision fc694b54fb0174e0913e6adf86691ef85a4ead47." }, "links": [ { "rel": "upstream", - "href": "https://github.com/Enntity/sparkqwen/tree/85325e5104b2a6864e0cf31ec470a8c3327d4de4" + "href": "https://github.com/Enntity/sparkqwen/tree/8b28467324893583838871b6a6b6a28d3059e64f" }, { "rel": "engine", @@ -219,7 +219,7 @@ "warmup": false, "bootstrap": { "adapter": "docker", - "image": "ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e", + "image": "ghcr.io/enntity/atlas-sparkqwen@sha256:9408884dc4bf2c84b7dd097d62effc73f58fffc25a1d67db73d0b4e69516a7c0", "pull": false, "pullTimeoutMs": 3600000, "createArgs": [ @@ -320,7 +320,7 @@ }, "bootstrap": { "adapter": "docker", - "image": "ghcr.io/enntity/atlas-sparkqwen@sha256:c3ea9f85bab9ff3599000b1e96f594714f3851e0b468da8bf2d10232e237e09e", + "image": "ghcr.io/enntity/atlas-sparkqwen@sha256:9408884dc4bf2c84b7dd097d62effc73f58fffc25a1d67db73d0b4e69516a7c0", "pull": false, "pullTimeoutMs": 3600000, "createArgs": [ diff --git a/test/atlas-sparkglm-recipe.test.mjs b/test/atlas-sparkglm-recipe.test.mjs index d222007..4d74e91 100644 --- a/test/atlas-sparkglm-recipe.test.mjs +++ b/test/atlas-sparkglm-recipe.test.mjs @@ -528,18 +528,18 @@ assert.equal(pins.model.revision, '423acf37583782c51c142d145aef733d72943d93'); assert.equal(pins.model.repo, 'nvidia/GLM-5.3-Flash-NVFP4'); assert.equal(pins.source.repo, 'Enntity/sparkglm'); assert.equal(pins.status, 'final'); -assert.equal(pins.source.revision, '3c7d67146929f350ed75fcdfd5e5349db843b483'); -assert.equal(pins.source.installTree, '246a3a948ab98d19e085bfd39797e554f0ee9261'); +assert.equal(pins.source.revision, '7a67d9bce4b5f408ad67f2a958ef1a8787136fdd'); +assert.equal(pins.source.installTree, 'f95a9af14236a781bb13f57dcc9ea0342f339905'); assert.equal(pins.source.buildScript, 'install/build.sh'); // The normal install path addresses the immutable registry digest, not the // mutable tag. The tag stays as source-build metadata only. assert.equal( pins.image.reference, - 'ghcr.io/enntity/atlas-sparkglm@sha256:e4d313b5e32c47545eab0059eea65a38e65d03fc8e0d5b8c2cfb497c39f0372a' + 'ghcr.io/enntity/atlas-sparkglm@sha256:78420594991e6e7e3914f84a6300ac1cf2f4e18b68e331f8ddc74fd7db905d3c' ); assert.match(pins.image.reference, IMAGE_REFERENCE_PATTERN); assert.equal(pins.image.reference.split('@')[0], IMAGE_REPOSITORY); -assert.equal(pins.image.tag, 'ghcr.io/enntity/atlas-sparkglm:246a3a948ab9'); +assert.equal(pins.image.tag, 'ghcr.io/enntity/atlas-sparkglm:f95a9af14236'); assert.equal(pins.image.label, 'io.enntity.sparkglm.install-tree'); assert.equal(pins.image.architecture, 'arm64'); assert.equal(pins.image.entrypoint, '/opt/atlas/serve.py'); @@ -547,7 +547,7 @@ assert.equal(pins.image.entrypoint, '/opt/atlas/serve.py'); assert(!Object.hasOwn(pins, 'installer')); assert(!Object.hasOwn(pins.image, 'tagPrefix')); assert.notEqual(pins.image.reference, pins.image.tag, 'the digest must not be the mutable tag'); -assert(!pins.image.reference.includes(':246a3a948ab9'), 'the tag must never appear as the pulled reference'); +assert(!pins.image.reference.includes(':f95a9af14236'), 'the tag must never appear as the pulled reference'); assert(pins.overlay.marker.includes('conversion.complete.json')); assert.deepEqual(verifyPins(pins), []); @@ -839,7 +839,7 @@ assert.match(missingImageContract.stderr, /missing the Atlas entrypoint\/profile for (const identity of [`arm64 ${'f'.repeat(40)}`, `amd64 ${pins.source.installTree}`, 'arm64 ']) { const mismatched = runInstaller({ ...fakeEnv, ATLAS_TEST_IDENTITY: identity }, ['--check-only']); assert.notEqual(mismatched.status, 0, `--check-only must reject image identity ${identity}`); - assert.match(mismatched.stderr, /expected 'arm64 246a3a948ab98d19e085bfd39797e554f0ee9261'/); + assert.match(mismatched.stderr, /expected 'arm64 f95a9af14236a781bb13f57dcc9ea0342f339905'/); } const noImageDir = fs.mkdtempSync(path.join(converterRoot, 'no-image-')); const checkedMissingImage = runInstaller({ ...fakeEnv, ATLAS_TEST_IMAGE_DIR: noImageDir }, ['--check-only']); diff --git a/test/atlas-sparkqwen-recipe.test.mjs b/test/atlas-sparkqwen-recipe.test.mjs index a52539b..e2354f8 100644 --- a/test/atlas-sparkqwen-recipe.test.mjs +++ b/test/atlas-sparkqwen-recipe.test.mjs @@ -323,10 +323,10 @@ assert.throws(() => materialize(missingFabric), /cluster\.nodes\[ennspark04\]\.f // ---- pins: final identities, pending digest, fail-closed gate --------------- assert.equal(pins.source.repo, 'Enntity/sparkqwen'); -assert.equal(pins.source.revision, '85325e5104b2a6864e0cf31ec470a8c3327d4de4'); -assert.equal(pins.source.installTree, '656234c336af4d3ba8d274f291d2033c31b998ce'); +assert.equal(pins.source.revision, '8b28467324893583838871b6a6b6a28d3059e64f'); +assert.equal(pins.source.installTree, '4ccd961ecf09d86c9f13087e42592789b5f56182'); assert.equal(pins.engine.commit, '1c43247d93ca050f02cb67b0603e88edfe3d92a5'); -assert.equal(pins.image.tag, 'ghcr.io/enntity/atlas-sparkqwen:656234c336af'); +assert.equal(pins.image.tag, 'ghcr.io/enntity/atlas-sparkqwen:4ccd961ecf09'); assert.equal(pins.image.tag, imageTagFor(pins)); assert.equal(pins.image.label, 'io.enntity.sparkqwen.install-tree'); assert.equal(pins.image.profilePath, '/opt/atlas/profiles/8x262k.json'); diff --git a/test/smoke.mjs b/test/smoke.mjs index 0f0a37d..cbc86f0 100644 --- a/test/smoke.mjs +++ b/test/smoke.mjs @@ -877,6 +877,7 @@ assert.deepEqual( 'linux-nvidia-dgx-spark-2x-deepseek-v4-flash-vision-mia-vllm', 'linux-nvidia-dgx-spark-2x-glm53-atlas', 'linux-nvidia-dgx-spark-2x-glm53-flash-exl3-vllm', + 'linux-nvidia-dgx-spark-2x-qwen38-flash-next-atlas', 'linux-nvidia-dgx-spark-2x-qwen38-flash-next-vllm', 'linux-nvidia-dgx-spark-cluster-flux2-klein-4b', 'linux-nvidia-dgx-spark-cluster-qwen3-embedding-4b-vllm', @@ -1055,7 +1056,7 @@ const recipeIndexReport = await buildRecipeIndexReport(config, { }); assert.equal(recipeIndexReport.ok, true); assert.equal(recipeIndexReport.index.id, 'lloom-community-recipes'); -assert.equal(recipeIndexReport.recipes.length, 46); +assert.equal(recipeIndexReport.recipes.length, 47); const indexedSparkRecipe = recipeIndexReport.recipes.find( (candidate) => candidate.id === 'linux-nvidia-gb10-qwen36-unsloth-vllm' );