Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8b57a99
feat(lucebox): hub CLI — install, serve, config, models
easel Jul 23, 2026
a083605
feat(lucebox): support AMD and heterogeneous GPU builds
davide221 Jul 27, 2026
5836946
fix(lucebox): harden config and target-only launches
davide221 Jul 27, 2026
4543199
fix(lucebox): align fallback config paths
davide221 Jul 27, 2026
40a04a3
fix(lucebox): harden the CLI package
davide221 Jul 27, 2026
f365f3d
test(lucebox): validate speculator symlinks
davide221 Jul 27, 2026
1ff2515
refactor(lucebox): tighten package edge cases
davide221 Jul 27, 2026
6f2a981
feat(lucebox): add guided inference menu
davide221 Jul 28, 2026
272cda3
feat(lucebox): harden guided optimization setup
davide221 Jul 29, 2026
e90528c
fix(lucebox): resolve final CLI review findings
davide221 Jul 29, 2026
d15fc1f
feat(lucebox): connect installed clients safely
davide221 Jul 29, 2026
e6d2ae5
fix(ci): include CMake test discovery in images
davide221 Jul 29, 2026
f3da3e1
feat(lucebox): automate heterogeneous inference placement
davide221 Jul 30, 2026
e7f2a0c
fix(lucebox): keep automatic plans within safe memory
davide221 Jul 30, 2026
0fb9a4c
feat(lucebox): focus the guided model library
davide221 Jul 31, 2026
187a55f
feat(cli): centralize model optimization planning
davide221 Jul 31, 2026
8215391
feat(lucebox): finalize model-aware automatic runtime
davide221 Aug 1, 2026
5229eba
fix(server): preserve multi-turn cache and tool replay
davide221 Aug 1, 2026
6e085c5
ci(images): split CUDA targets by architecture
davide221 Aug 1, 2026
92d31d1
test(lucebox): assert Strix UMA reserve semantics
davide221 Aug 1, 2026
76c227c
feat(lucebox): add safe on-machine calibration
davide221 Aug 1, 2026
87ade52
fix(lucebox): harden long-context optimization safety
davide221 Aug 2, 2026
65887ec
fix(build): keep BSA flag initialization goto-safe
davide221 Aug 2, 2026
d53e866
Merge branch 'main' into feat/lucebox-cli
davide221 Aug 5, 2026
1b24942
fix: address remaining Cubic P1/P2 review findings
davide221 Aug 5, 2026
add6cb5
test(harness): non-empty GGUF stubs for timeout regressions
davide221 Aug 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,35 @@ jobs:
- name: Lint Python surfaces touched by lucebox tooling
run: uv run --frozen --extra dev ruff check .

- name: Install shellcheck (for bash test runner)
# ubuntu-latest typically ships shellcheck pre-installed, but pin
# the dependency explicitly so the bash test runner can always rely
# on `command -v shellcheck` succeeding.
run: |
if ! command -v shellcheck >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y shellcheck
fi
shellcheck --version | head -3

- name: Typecheck lucebox CLI
run: uv run --frozen --extra dev python -m mypy --package lucebox

- name: Unit-test lucebox CLI
# The fast workspace sync above is enough: the suite mocks the
# docker / HTTP surfaces, so no torch wheel or GPU is needed.
# Keeps the lucebox Python honest on every push.
run: uv run --frozen --extra dev pytest lucebox -q
Comment thread
davide221 marked this conversation as resolved.

- name: Smoke-test lucebox.sh wrapper
# Catches `set -u` regressions, syntax errors, and stale dispatch
# handlers in the host-side wrapper + the in-container entrypoint.
# Runs shellcheck --severity=error across every shipped .sh file,
# exercises every subcommand dispatch under `set -u`, and drives the
# entrypoint's draft-resolution block through every family-glob
# branch — all on the bare runner without docker/nvidia/systemd.
run: bash scripts/test_lucebox_sh.sh

build:
name: Build (cmake + uv sync --extra megakernel)
runs-on: ubuntu-latest
Expand Down
21 changes: 15 additions & 6 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Docker prebuilds

# Builds the cuda12 lucebox-hub Docker image defined in docker-bake.hcl
# and pushes it to GHCR. The bake file is the source of
# Builds the cuda12, cuda128/RTX5090, cuda13/GB10, and ROCm images defined in
# docker-bake.hcl and pushes them to GHCR. The bake file is the source of
# truth for arch matrices and CUDA pinning; this workflow only handles
# fetching submodules, freeing runner disk, signing in to the registry, and
# wiring the cache.
Expand All @@ -12,7 +12,7 @@ on:
# event=tag` + `type=semver` rules below.
release:
types: [published]
# Build + push the rolling `:cuda12` / `:rocm` tags when an image-affecting
# Build + push the rolling CUDA / ROCm tags when an image-affecting
# file changes on main, so the public images track main without a ~2h
# rebuild on every unrelated commit (docs, harness, server tweaks that
# don't reach the image). Same paths as the PR guard below. The
Expand Down Expand Up @@ -90,14 +90,23 @@ jobs:
# halve wall time.
# • A self-hosted runner with the host's nvcc avoids the
# containerised CUDA toolkit pull entirely.
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
variant: [cuda12, rocm]
include:
- variant: cuda12
runner: ubuntu-latest
- variant: cuda128
runner: ubuntu-latest
- variant: cuda13
# Native arm64 compilation avoids hours of QEMU-emulated nvcc.
runner: ubuntu-24.04-arm
- variant: rocm
runner: ubuntu-latest
steps:
- name: Free runner disk space
# The default ubuntu-latest image keeps ~25 GB of preinstalled
Expand Down Expand Up @@ -190,7 +199,7 @@ jobs:
# under the concurrency group's pre-emption window. Release / main /
# dispatch builds keep the full consumer-GPU list so the published
# image runs on every supported card.
DFLASH_CUDA_ARCHES: ${{ github.event_name == 'pull_request' && '86' || '75;80;86;89;90;120' }}
DFLASH_CUDA_ARCHES: ${{ github.event_name == 'pull_request' && '86' || '75;80;86;89;90' }}
# Same split for HIP: PR builds compile gfx1151 only (the verified
# reference, Strix Halo); main / release builds widen to consumer
# RDNA so the published :rocm runs on RX 7900 (gfx1100) and both RDNA4
Expand Down
67 changes: 59 additions & 8 deletions .github/workflows/speed-profile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ name: Speed Profile

# Report-only speed profile for the inference engine. Runs on the self-hosted
# RTX 3090 (lucebox3) on PRs that touch the engine or the optimizations, and on
# manual dispatch. It NEVER blocks a PR (continue-on-error: true) — it publishes a
# report to the run summary + uploads the JSON / markdown / nsys trace as artifacts.
# manual dispatch. Benchmark/runtime failures are warnings, not a red PR check;
# the job publishes a summary plus JSON / markdown / nsys artifacts for triage.
#
# Why report-only: perf has run-to-run variance (thermals, clocks, scheduling).
# Gating a merge on a noisy absolute number produces false failures. We surface the
Expand All @@ -30,7 +30,9 @@ jobs:
name: Speed profile (self-hosted RTX 3090, sm_86)
runs-on: [self-hosted, gpu, sm86]
timeout-minutes: 30
continue-on-error: true # report-only: a slow/failed profile must not block the PR
# Individual benchmark failures are converted into warnings below. Keeping
# the job itself green avoids a misleading red PR check for a report-only
# measurement while still preserving logs and artifacts for triage.

# Model paths live on the runner, not in the repo (multi-GB weights). They are
# overridable via repo variables so the runner owner can point at whatever is
Expand All @@ -51,7 +53,7 @@ jobs:

- name: GPU info (and pin clocks to cut variance, if permitted)
run: |
nvidia-smi --query-gpu=name,driver_version,memory.total,power.limit --format=csv
nvidia-smi --query-gpu=name,driver_version,memory.total,memory.free,power.limit --format=csv
# Locking clocks makes the numbers comparable run-to-run. Safe to skip if the
# runner user can't run nvidia-smi -lgc; the profiler still records the power cap.
sudo nvidia-smi -lgc 1395 2>/dev/null || echo "clock lock not permitted; continuing"
Expand Down Expand Up @@ -94,8 +96,43 @@ jobs:
echo "::warning title=Speed profile skipped::Model weights not found under $MODELS — see the run summary."
fi

- name: Build engine binaries (sm_86, Release)
- name: Wait for exclusive GPU capacity
id: capacity
if: steps.models.outputs.present == 'true'
env:
MIN_FREE_MIB: ${{ vars.LUCEBOX_SPEED_MIN_FREE_MIB || '22000' }}
run: |
# The 27B target + draft fit on a clean 24 GB 3090, but fail with a
# cryptic cudaMalloc error when another process temporarily owns VRAM.
# Never kill a user's process: wait briefly, then skip this report.
ready=false
free_mib=0
for attempt in {1..30}; do
free_mib=$(nvidia-smi --query-gpu=memory.free \
--format=csv,noheader,nounits | sed -n '1p' | tr -d '[:space:]')
if [[ "$free_mib" =~ ^[0-9]+$ ]] && [ "$free_mib" -ge "$MIN_FREE_MIB" ]; then
ready=true
break
fi
if [ "$attempt" -eq 1 ]; then
echo "Only ${free_mib:-unknown} MiB is free; waiting for ${MIN_FREE_MIB} MiB."
fi
sleep 10
done
echo "ready=$ready" >> "$GITHUB_OUTPUT"
echo "free_mib=${free_mib:-0}" >> "$GITHUB_OUTPUT"
if [ "$ready" != true ]; then
echo "::warning title=Speed profile skipped::GPU remained busy (${free_mib:-unknown} MiB free; ${MIN_FREE_MIB} MiB required)."
{
echo "## 🏎️ Speed profile — skipped (GPU busy)"
echo ""
echo "The RTX 3090 had only \`${free_mib:-unknown} MiB\` free after a five-minute wait;"
echo "the model-backed profile requires \`${MIN_FREE_MIB} MiB\`. No process was killed."
} >> "$GITHUB_STEP_SUMMARY"
fi

- name: Build engine binaries (sm_86, Release)
if: steps.models.outputs.present == 'true' && steps.capacity.outputs.ready == 'true'
run: |
cd server
cmake -B build \
Expand All @@ -106,7 +143,7 @@ jobs:
cmake --build build --target test_dflash test_generate -j"$(nproc)"

- name: Install profiler Python deps (isolated, pinned venv)
if: steps.models.outputs.present == 'true'
if: steps.models.outputs.present == 'true' && steps.capacity.outputs.ready == 'true'
run: |
cd server
# The profiler only needs a tokenizer, so we use a tiny isolated venv
Expand All @@ -123,7 +160,8 @@ jobs:
protobuf==6.31.1

- name: Run speed profiler
if: steps.models.outputs.present == 'true'
id: profile
if: steps.models.outputs.present == 'true' && steps.capacity.outputs.ready == 'true'
run: |
cd server
# Use a committed baseline if one is staged so the report can flag a
Expand All @@ -143,6 +181,7 @@ jobs:
# The nsys pass adds a separate short profiled run; tok/s is measured on clean passes. Run 5
# timing reps by default so the report can distinguish real deltas from
# thermal/clock jitter; repo variables can trim this for temporary smoke runs.
set +e
.profiler-venv/bin/python scripts/profile.py \
--target "$MODELS/$TARGET_MODEL" \
--draft "$MODELS/$DRAFT_MODEL" \
Expand All @@ -153,6 +192,18 @@ jobs:
--nsys --check-lossless \
"${baseline_arg[@]}" \
--out-json profile.json --out-md profile.md
profile_rc=$?
set -e
echo "exit_code=$profile_rc" >> "$GITHUB_OUTPUT"
if [ "$profile_rc" -ne 0 ]; then
echo "::warning title=Speed profiler failed::Profiler exited $profile_rc; see its logs and uploaded artifacts."
{
echo "## 🏎️ Speed profile — profiler failed"
echo ""
echo "The report-only profiler exited with code \`$profile_rc\`. See the job log for the root cause."
} >> "$GITHUB_STEP_SUMMARY"
fi
exit 0
env:
LUCEBOX_SPEED_BASELINE: ${{ vars.LUCEBOX_SPEED_BASELINE || '' }}
LUCEBOX_SPEED_REGRESS_PCT: ${{ vars.LUCEBOX_SPEED_REGRESS_PCT || '' }}
Expand All @@ -161,7 +212,7 @@ jobs:
LUCEBOX_SPEED_NOISE_RSD_PCT: ${{ vars.LUCEBOX_SPEED_NOISE_RSD_PCT || '' }}

- name: Publish report to the run summary
if: always() && steps.models.outputs.present == 'true'
if: always() && steps.models.outputs.present == 'true' && steps.capacity.outputs.ready == 'true'
run: |
if [ -f server/profile.md ]; then
{ echo "## 🏎️ Speed profile"; echo ""; cat server/profile.md; } >> "$GITHUB_STEP_SUMMARY"
Expand Down
47 changes: 29 additions & 18 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,31 @@

# ─── Stage 1: builder ───────────────────────────────────────────────────────
# CUDA_VERSION / UBUNTU_VERSION / DFLASH_CUDA_ARCHES are build args so the
# same Dockerfile can be repinned later. The prebuilt image is the
# CUDA 12.8 path:
# • lucebox-hub:cuda12 — CUDA 12.8.1, sm_75;80;86;89;90;120
# same Dockerfile serves all published CUDA variants:
# • lucebox-hub:cuda12 — CUDA 12.0.1, x86_64, sm_75;80;86;89;90
# • lucebox-hub:cuda128 — CUDA 12.8.1, x86_64, sm_120 (RTX 5090)
# • lucebox-hub:cuda13 — CUDA 13.0.1, arm64, sm_121 (GB10/DGX Spark)
# See docker-bake.hcl for the canonical invocation.
ARG CUDA_VERSION=12.8.1
ARG CUDA_VERSION=12.0.1
ARG UBUNTU_VERSION=22.04
FROM nvidia/cuda:${CUDA_VERSION}-devel-ubuntu${UBUNTU_VERSION} AS builder

ARG DEBIAN_FRONTEND=noninteractive

# Fat-binary CUDA arch list, semicolon-separated. Defaults cover the CUDA 12.8
# image. dflash-supported arches in this image:
# Fat-binary CUDA arch list, semicolon-separated. Defaults cover the broadly
# compatible CUDA 12 image. dflash-supported arches in this image:
# 75 Turing RTX 2080 Ti
# 80 Ampere A100
# 86 Ampere RTX 3090, A40, A10
# 89 Ada RTX 4090, L40
# 90 Hopper H100
# 120 Blackwell RTX 5090, RTX 5090 Laptop
# Thor and GB10 prebuilt-image coverage is intentionally omitted.
# RTX 5090 (sm_120) and GB10 (sm_121) are built by docker-bake.hcl's
# CUDA 12.8 and CUDA 13 targets, respectively.
# Pre-Turing arches (sm_60/61/70/72) are intentionally excluded — dflash's
# BF16/WMMA paths have no fallback below sm_75. Each arch adds ~50-200 MB
# of fat-binary kernel code and ~3-5 min of nvcc time per .cu translation
# unit.
ARG DFLASH_CUDA_ARCHES="75;80;86;89;90;120"
ARG DFLASH_CUDA_ARCHES="75;80;86;89;90"

RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
Expand Down Expand Up @@ -100,16 +101,18 @@ RUN cmake -S /src/server -B /src/server/build \
-DCMAKE_BUILD_WITH_INSTALL_RPATH=ON \
-DDFLASH27B_USER_CUDA_ARCHITECTURES="${DFLASH_CUDA_ARCHES}" \
-DCMAKE_CUDA_ARCHITECTURES="${DFLASH_CUDA_ARCHES}" \
&& cmake --build /src/server/build --target test_dflash dflash_server test_server_unit --parallel
&& cmake --build /src/server/build \
--target test_dflash dflash_server backend_ipc_daemon test_server_unit --parallel

# Prune the build tree to only what the runtime stage needs: the native server,
# test_dflash, test_server_unit, and the ggml shared libs their embedded rpath
# backend IPC companion, tests, and the ggml shared libs their embedded rpath
# ($ORIGIN/deps/...) looks up. Drops ~1 GB per image of CMakeFiles/,
# libdflash27b.a (statically linked into the binaries), ninja state,
# compile_commands.json, and the template-instance .o tree from ggml-cuda.
RUN cd /src/server/build \
&& find . -mindepth 1 -maxdepth 1 \
! -name test_dflash ! -name dflash_server ! -name test_server_unit ! -name deps -exec rm -rf {} + \
! -name test_dflash ! -name dflash_server ! -name backend_ipc_daemon \
! -name test_server_unit ! -name deps -exec rm -rf {} + \
&& find deps -mindepth 1 -type f ! -name 'lib*.so*' -delete \
&& find deps -depth -type d -empty -delete

Expand All @@ -118,14 +121,16 @@ RUN cd /src/server/build \
# of these reuses the cached CUDA layers above and only re-runs the
# runtime stage's uv sync (~70s) instead of the full ~25-minute build.
#
# Host-side Python tooling (lucebox/, harness/) is intentionally not copied
# here: this image is the server. Such tooling can layer on top later via a
# follow-up COPY directive or a runtime bind-mount during dev.
# The lucebox CLI is a workspace member (root pyproject + uv.lock), so its
# source must be present for `uv sync --frozen` in the runtime stage. It runs
# inside the container — the host wrapper `docker exec`s into it.
COPY pyproject.toml uv.lock README.md /src/
COPY server/pyproject.toml server/README.md /src/server/
COPY server/scripts /src/server/scripts
COPY optimizations/pflash /src/optimizations/pflash
COPY optimizations/megakernel /src/optimizations/megakernel
COPY lucebox/pyproject.toml lucebox/README.md /src/lucebox/
COPY lucebox/src /src/lucebox/src

# ─── Stage 2: runtime ───────────────────────────────────────────────────────
# Runtime image: ships nvidia driver libs but no nvcc / dev headers. Matches
Expand Down Expand Up @@ -180,9 +185,9 @@ COPY --from=builder /src/optimizations/megakernel/pyproject.toml \
/src/optimizations/megakernel/README.md \
/opt/lucebox-hub/optimizations/megakernel/

# Host-side Python tooling (lucebox/, harness/) is intentionally absent
# here: this image is the server base layer. Such tooling can layer on top
# later via a follow-up COPY directive or a runtime bind-mount during dev.
# The lucebox CLI package (a workspace member) — installed by the uv sync
# below and invoked in-container by the host wrapper via `docker exec`.
COPY --from=builder /src/lucebox /opt/lucebox-hub/lucebox

# server: ship the entrypoint/benchmark scripts, the pyproject + README that uv
# resolves against, and the pruned build tree (binaries + .so files from the
Expand All @@ -205,12 +210,18 @@ COPY --from=builder /src/server/build /opt/lucebox-hub/server/build
# server/share/model_cards. The canonical copy also lives at
# /opt/lucebox-hub/share/model_cards for any host-side tooling.
COPY share/model_cards /opt/lucebox-hub/share/model_cards
# Dedicated targets for narrow, read-only binds when a selected model is a
# symlink to storage outside the main models directory.
RUN mkdir -p /opt/lucebox-hub/server/share \
/opt/lucebox-resolved/target \
/opt/lucebox-resolved/draft \
/opt/lucebox-resolved/draft-dir \
&& ln -s /opt/lucebox-hub/share/model_cards \
/opt/lucebox-hub/server/share/model_cards

RUN test -x /opt/lucebox-hub/server/build/test_dflash \
&& test -x /opt/lucebox-hub/server/build/dflash_server \
&& test -x /opt/lucebox-hub/server/build/backend_ipc_daemon \
&& test -x /opt/lucebox-hub/server/build/test_server_unit \
&& test -f /opt/lucebox-hub/server/share/model_cards/qwen3.6-27b.json \
&& chmod +x /opt/lucebox-hub/server/scripts/entrypoint.sh
Expand Down
Loading
Loading