Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 88 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,14 @@
name: CI

# NOTE: no `paths-ignore`. Every job here is (or can be) a *required* status
# check, and a required check that never runs leaves a PR stuck on "Expected —
# waiting for status to be reported" forever. So CI always runs on every PR to
# main; the cargo cache keeps doc-only re-runs cheap (nothing recompiles).
on:
push:
branches: [main]
paths-ignore:
- '**/*.md'
- 'docs/**'
- 'LICENSE*'
- '.github/ISSUE_TEMPLATE/**'
pull_request:
branches: [main]
paths-ignore:
- '**/*.md'
- 'docs/**'
- 'LICENSE*'
- '.github/ISSUE_TEMPLATE/**'

env:
CARGO_TERM_COLOR: always
Expand All @@ -25,6 +19,7 @@ jobs:
name: CI
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, ubuntu-24.04-arm]
steps:
Expand All @@ -51,3 +46,86 @@ jobs:
cargo clean -p lattice-tune 2>/dev/null || true
cargo clean -p lattice-transport 2>/dev/null || true
- run: ./scripts/ci.sh

# The default `cargo test/clippy --workspace` only ever compiles DEFAULT
# features. Feature-gated code (safetensors serialization, the inference-hook
# bridge, train-backward, metal-gpu kernels) is invisible to it — that is how
# two real LoRA save/load bugs shipped in `safetensors`-gated code that
# default CI never built. This job compiles the gated surfaces so they cannot
# silently rot. `--no-run` on test targets = type-check + codegen without
# spending runner time executing.
feature-matrix:
name: feature-matrix
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.1
- name: Pin the real toolchain bin on PATH
run: dirname "$(rustup which cargo)" >> "$GITHUB_PATH"
- uses: Swatinem/rust-cache@v2
with:
shared-key: feature-matrix
cache-on-failure: true
- name: tune — safetensors + inference-hook + serde (compile tests)
run: cargo test -p lattice-tune --features safetensors,inference-hook,serde --no-run
- name: tune — train-backward (compile tests)
run: cargo test -p lattice-tune --features train-backward --no-run
- name: inference — f16 + train-backward
run: cargo build -p lattice-inference --features f16,train-backward
- name: embed — local
run: cargo build -p lattice-embed --features local
- name: fann — no default features
run: cargo build -p lattice-fann --no-default-features
# Metal kernels only exist on macOS; gate so Linux does not try to build them.
- name: inference — metal-gpu (macOS only)
if: runner.os == 'macOS'
run: cargo build -p lattice-inference --features metal-gpu
- name: embed — metal-gpu (macOS only)
if: runner.os == 'macOS'
run: cargo build -p lattice-embed --features metal-gpu

# The benchmark harnesses live behind their own feature flags / target gates
# and are never built by `cargo test`. A `Qwen35Config` field added to the
# struct but not to the bench's literal initializer (exactly what happened
# with `quarot_rotation_seed`) only surfaces when the benches are compiled.
# Runs on Apple Silicon because the inference benches are NEON (aarch64) code;
# on x86 they are cfg'd out and the gate would be vacuous.
bench-compile:
name: bench-compile
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.94.1
- name: Pin the real toolchain bin on PATH
run: dirname "$(rustup which cargo)" >> "$GITHUB_PATH"
- uses: Swatinem/rust-cache@v2
with:
shared-key: bench-compile
cache-on-failure: true
- name: inference benches (bench-internals)
run: cargo bench -p lattice-inference --features bench-internals --no-run
- name: embed benches
run: cargo bench -p lattice-embed --no-run

# Supply-chain gate. licenses/bans/sources are deterministic and REQUIRED:
# a green main cannot turn red without a manifest/lockfile change. Advisories
# run separately as informational (continue-on-error) because fresh RUSTSEC
# entries land with no change of ours and must not wedge merges.
cargo-deny:
name: cargo-deny
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: licenses + bans + sources (required)
uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check licenses bans sources
- name: advisories (informational)
uses: EmbarkStudios/cargo-deny-action@v2
continue-on-error: true
with:
command: check advisories
79 changes: 71 additions & 8 deletions .github/workflows/e2e-parity.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ name: e2e-parity
# Replaces bench-regression (ADR-058 revision): live HF-vs-lattice parity on
# the same runner. Reference runs first (warms the machine), lattice second.
# Gates on greedy token agreement, reports speed informationally.
#
# Required-check shape: the workflow ALWAYS triggers (no `paths:` filter on the
# PR event), but the expensive macOS `parity` job only runs when engine files
# actually change. A cheap always-running `parity-gate` job is the *required*
# status context — it passes when there is nothing to check and mirrors the
# parity result when there is. This avoids the classic trap where a required
# check is path-filtered, never reports on unrelated PRs, and wedges them on
# "Expected — waiting for status to be reported".

on:
pull_request:
branches: [main]
paths:
- 'crates/inference/src/**'
- 'crates/embed/src/**'
- 'crates/inference/Cargo.toml'
- 'crates/embed/Cargo.toml'
- 'Cargo.lock'
- 'scripts/e2e_parity_check.py'
- '.github/workflows/e2e-parity.yml'
push:
branches: [main]
paths:
Expand All @@ -32,8 +32,41 @@ env:
CARGO_INCREMENTAL: '0'

jobs:
# Decide whether the engine surface changed. Output drives both the expensive
# parity job and the required gate below.
changes:
name: detect-engine-changes
runs-on: ubuntu-latest
outputs:
engine: ${{ steps.filter.outputs.engine }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: filter
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
git fetch origin "${{ github.base_ref }}" --depth=1 2>/dev/null || true
CHANGED=$(git diff --name-only "origin/${{ github.base_ref }}...HEAD" \
|| git diff --name-only "HEAD~1...HEAD")
else
CHANGED=$(git diff --name-only "HEAD~1...HEAD" 2>/dev/null || true)
fi
echo "Changed files:"
echo "$CHANGED"
if echo "$CHANGED" | grep -qE '^crates/(inference|embed)/src/|^crates/(inference|embed)/Cargo\.toml$|^Cargo\.lock$|^scripts/e2e_parity_check\.py$|^\.github/workflows/e2e-parity\.yml$'; then
echo "engine=true" >> "$GITHUB_OUTPUT"
echo "→ engine surface changed: parity REQUIRED"
else
echo "engine=false" >> "$GITHUB_OUTPUT"
echo "→ no engine change: parity not required"
fi

parity:
name: e2e parity
needs: changes
if: needs.changes.outputs.engine == 'true'
runs-on: macos-latest
timeout-minutes: 15
steps:
Expand Down Expand Up @@ -101,3 +134,33 @@ jobs:
with:
header: e2e-parity
path: parity-report.md

# REQUIRED status check. Always runs, always reports. Passes when the engine
# did not change (nothing to verify); mirrors the parity job's verdict when it
# did. Fails closed if change-detection itself errored.
parity-gate:
name: parity-gate
needs: [changes, parity]
if: always()
runs-on: ubuntu-latest
steps:
- name: Resolve gate
run: |
CHANGES_RESULT="${{ needs.changes.result }}"
ENGINE="${{ needs.changes.outputs.engine }}"
PARITY_RESULT="${{ needs.parity.result }}"
echo "changes=$CHANGES_RESULT engine=$ENGINE parity=$PARITY_RESULT"
if [ "$CHANGES_RESULT" != "success" ]; then
echo "::error::change-detection did not succeed — failing closed."
exit 1
fi
if [ "$ENGINE" != "true" ]; then
echo "No engine change — parity not required. PASS."
exit 0
fi
if [ "$PARITY_RESULT" = "success" ]; then
echo "Engine changed and parity PASSED."
exit 0
fi
echo "::error::Engine changed and parity did not succeed (result=$PARITY_RESULT)."
exit 1
10 changes: 10 additions & 0 deletions crates/inference/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,19 @@ harness = false
name = "e2e_bench"
harness = false

# Disabled (bench = false) pending revival. This is a counting-allocator
# baseline (OPT-002..005) that asserts exact per-op allocation indices. It was
# authored against an f32 FlatKVCache; the lib has since migrated KV storage to
# f16, so the cache read/write sites no longer typecheck. The naive fix —
# f32<->f16 conversions at the cache boundary — would inject the very
# allocations this bench counts, silently corrupting every measurement. Reviving
# it means re-deriving the alloc baseline against the f16 decode path, which
# belongs in a dedicated perf PR with bench output, not the CI-hardening gate.
# Tracked: GTD lattice "revive inference_perf alloc baseline against f16 KV cache".
[[bench]]
name = "inference_perf"
harness = false
bench = false

[[bench]]
name = "topk_readback"
Expand Down
1 change: 1 addition & 0 deletions crates/inference/src/forward/neon_forward.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,7 @@ pub mod bench_support {
max_position_embeddings: 512,
mtp_num_hidden_layers: 0,
mtp_use_dedicated_embeddings: false,
quarot_rotation_seed: None,
};

let rope_dim = (head_dim as f32 * cfg.partial_rotary_factor) as usize; // 32
Expand Down
61 changes: 61 additions & 0 deletions deny.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# cargo-deny configuration — supply-chain gate for the lattice workspace.
# Enforced in CI by `.github/workflows/ci.yml` (job `cargo-deny`) via
# EmbarkStudios/cargo-deny-action@v2.
#
# Split of concern (deliberate — keeps the merge gate smooth, never wedges):
# - licenses / bans / sources → REQUIRED (deterministic; a green main cannot
# spontaneously turn red without a manifest or lockfile change).
# - advisories → INFORMATIONAL (run with continue-on-error in
# CI). New RUSTSEC entries land continuously with no code change of ours, so
# gating merges on them would let an unrelated upstream disclosure block the
# whole pipeline. They still surface in the job log for triage.

[graph]
all-features = true

[licenses]
version = 2
# Confidence below this for a detected license text is treated as "no license".
confidence-threshold = 0.8
# Every entry below is genuinely present in the dependency tree (verified via
# `cargo metadata`). The set is exactly: the licenses that appear standalone or
# inside an `AND` (so they are *forced*), plus the permissive OR-alternatives
# already in the tree. No copyleft: LGPL-2.1 appears only as an OR alternative
# (MIT OR Apache-2.0 OR LGPL-2.1-or-later) and is never the only choice.
allow = [
"MIT",
"MIT-0",
"Apache-2.0",
"Apache-2.0 WITH LLVM-exception",
"BSD-2-Clause",
"BSD-3-Clause",
"0BSD",
"ISC",
"Zlib",
"Unicode-3.0",
"BSL-1.0",
"CC0-1.0",
"CDLA-Permissive-2.0",
"Unlicense",
]

[bans]
# Duplicate versions are ubiquitous in any non-trivial tree; surface them as a
# warning rather than a hard failure so the gate stays smooth. Tighten later if
# we want to actively dedupe.
multiple-versions = "warn"
wildcards = "warn"

[sources]
# Verified clean: every package in Cargo.lock resolves to crates.io, zero git
# deps. Deny anything off the registry so a surprise source is caught.
unknown-registry = "deny"
unknown-git = "deny"

[advisories]
version = 2
# Yanked crates are a real, deterministic problem — deny them. Vulnerability
# advisories are denied by default under the v2 schema; in CI this whole check
# runs continue-on-error (informational), so a fresh upstream disclosure cannot
# wedge merges while still showing up in the log.
yanked = "deny"
Loading