From a1806495d4773fd0ebc11b314d9679ae61644744 Mon Sep 17 00:00:00 2001 From: xiaguan <751080330@qq.com> Date: Sat, 4 Jul 2026 05:16:32 -0400 Subject: [PATCH 1/3] fix(glm52): restore cfg(test) imports of Glm52ProjBytes PR #550 marked from_host/upload test-only but dropped the imports they need, breaking cargo test -p openinfer-glm52 --lib on main. Co-Authored-By: Claude Fable 5 --- openinfer-glm52/src/dense.rs | 2 ++ openinfer-glm52/src/indexer.rs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/openinfer-glm52/src/dense.rs b/openinfer-glm52/src/dense.rs index f71db1da..e0f630a1 100644 --- a/openinfer-glm52/src/dense.rs +++ b/openinfer-glm52/src/dense.rs @@ -10,6 +10,8 @@ use cudarc::driver::CudaSlice; use half::bf16; use openinfer_kernels::tensor::DeviceContext; +#[cfg(test)] +use crate::fp8::Glm52ProjBytes; use crate::fp8::{Glm52MlpScratch, ProjWeight, fp8_mlp_into, pack_proj_pair}; const HIDDEN: usize = 6144; diff --git a/openinfer-glm52/src/indexer.rs b/openinfer-glm52/src/indexer.rs index 5ae5ec66..5e23a94b 100644 --- a/openinfer-glm52/src/indexer.rs +++ b/openinfer-glm52/src/indexer.rs @@ -45,9 +45,9 @@ use openinfer_kernels::ops::{ }; use openinfer_kernels::tensor::DeviceContext; -#[cfg(test)] -use crate::fp8::fp8_linear; use crate::fp8::{FP8_BLOCK, ProjWeight, fp8_linear_into}; +#[cfg(test)] +use crate::fp8::{Glm52ProjBytes, fp8_linear}; const HIDDEN: usize = 6144; const Q_LORA: usize = 2048; From 2a9206a1e4d7b61dabeb0755d6ff5603d68cff2d Mon Sep 17 00:00:00 2001 From: xiaguan <751080330@qq.com> Date: Sat, 4 Jul 2026 05:17:41 -0400 Subject: [PATCH 2/3] chore(features): standardize model feature flags, drop crate-internal gates Codify the feature standard in docs/conventions/feature-flags.md: one feature name per model line, selection lives in openinfer-server and openinfer-kernels only. - Tier A (pure-CUDA kernels): model crates carry no self-named feature and always compile whole. Migrate deepseek-v2-lite (14 cfg gates, optional kernels dep, reject-only stubs) and kimi-k2 (8 cfg gates, stub, kernel-report no longer implies a model feature) to the pattern #550 established for glm52. - Tier B (build-time Python toolchains): the only allowed gate is a whole-crate #![cfg]. Collapse deepseek-v4's 10 scattered item gates into one, matching qwen35-4b. - Update the pre-commit kimi hook, bench_dsv2lite_vllm_matrix.py, and living gate docs that passed the removed crate-level features. Verified: workspace --lib tests, server check per feature (default, kimi-k2, deepseek-v2-lite, glm52), model-crate --bins --tests checks, both local clippy hooks with -D warnings. Co-Authored-By: Claude Fable 5 --- .pre-commit-config.yaml | 11 ++-- docs/conventions/feature-flags.md | 64 +++++++++++++++++++ docs/index.md | 1 + .../decode-attribution-gate.md | 7 +- .../device-resident-nccl-combine.md | 11 ++-- .../deepseek-v2-lite/hf-accuracy-gate.md | 4 +- docs/models/deepseek-v2-lite/source-layout.md | 2 +- docs/models/kimi-k2/accuracy-gate.md | 2 +- openinfer-deepseek-v2-lite/Cargo.toml | 8 +-- openinfer-deepseek-v2-lite/src/lib.rs | 28 -------- openinfer-deepseek-v4/src/lib.rs | 15 ++--- openinfer-kernels/Cargo.toml | 8 ++- openinfer-kimi-k2/Cargo.toml | 5 +- openinfer-kimi-k2/src/config.rs | 4 -- openinfer-kimi-k2/src/lib.rs | 16 ----- openinfer-kimi-k2/tests/vllm_golden_gate.rs | 7 +- openinfer-server/Cargo.toml | 9 ++- scripts/bench_dsv2lite_vllm_matrix.py | 8 +-- 18 files changed, 110 insertions(+), 100 deletions(-) create mode 100644 docs/conventions/feature-flags.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 618f0d6d..2d6b6126 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,15 +19,16 @@ repos: args: [--release, --all-targets] # The hook above lints `default-members` (openinfer-server) with default - # features, so the feature-gated kimi-k2 crate is never compiled and never - # linted. This local hook closes that hole — but only when kimi-k2 source - # changes (the CUDA build is expensive), and `--no-deps` keeps it scoped to - # the crate itself rather than its shared workspace dependencies. + # features, whose dep graph does not include the kimi-k2 crate, so it is + # never compiled and never linted there. This local hook closes that hole — + # but only when kimi-k2 source changes (the CUDA build is expensive), and + # `--no-deps` keeps it scoped to the crate itself rather than its shared + # workspace dependencies. - repo: local hooks: - id: clippy-kimi-k2 name: clippy (kimi-k2, -D warnings) - entry: cargo clippy -p openinfer-kimi-k2 --no-deps --release --features kimi-k2,kernel-report --all-targets -- -D warnings + entry: cargo clippy -p openinfer-kimi-k2 --no-deps --release --features kernel-report --all-targets -- -D warnings language: system types: [rust] files: ^openinfer-kimi-k2/ diff --git a/docs/conventions/feature-flags.md b/docs/conventions/feature-flags.md new file mode 100644 index 00000000..d8d968a4 --- /dev/null +++ b/docs/conventions/feature-flags.md @@ -0,0 +1,64 @@ +# Cargo feature-flag convention + +**TL;DR**: One feature name per model line, spelled identically in `openinfer-server` and `openinfer-kernels`. A model crate itself carries **no** self-named feature — unless its kernels need a build-time Python toolchain, in which case the *only* allowed gate is a single whole-crate `#![cfg(feature = "...")]`. Capability features (`moe`, `pplx-ep`, `kernel-report`, …) are named after the capability, never after a model. + +## Why features exist here at all + +A feature answers exactly one question: **does this binary compile this model line's kernels?** Everything else — which model actually runs, EP topology, backend selection — is runtime configuration, not a feature. Two forces shape the rules: + +1. `openinfer-kernels/build.rs` is expensive (nvcc, and for some lines AOT codegen through Python). Features scope what it compiles. +2. Two model lines cannot build without external toolchains (`qwen35-4b`: Python + Triton; `deepseek-v4`: Python + TileLang + CuTe DSL). A featureless `cargo build --workspace` must stay buildable on a machine that has CUDA but no Python — so those two, and only those two, stay gated at the crate level. + +## The rules + +### 1. Feature names + +The feature for a model line is the crate-name suffix: `openinfer-kimi-k2` → `kimi-k2`. The same string is used in `openinfer-server` and `openinfer-kernels`. Capability features describe the capability (`moe`, `pplx-ep`, `kernel-call-trace`, `kernel-report`, `deepseek-v4-cutedsl-diagnostic`), and may imply model features but are never a synonym for one. + +### 2. `openinfer-kernels` — one feature per model's kernels + +Each model line gets a same-named feature that scopes its `csrc//` sources (and AOT codegen, if any). Shared substrate gets a capability feature that model features imply (`glm52 = ["moe"]`, `kimi-k2 = ["moe"]`). Features that need more than nvcc must say so in a comment next to the declaration. + +### 3. Model crates — Tier A (pure CUDA): no self feature + +Applies to: **qwen3, glm52, kimi-k2, deepseek-v2-lite** — anything whose kernels need only nvcc. + +- No self-named feature. The crate always compiles its full self. +- `openinfer-kernels = { workspace = true, features = [""] }`, **not** optional. +- No `#[cfg(feature = "")]` anywhere in the crate, no reject-only stub functions, no `required-features` on tests/bins (except for genuine capability features like `kernel-report`). +- Cost accepted knowingly: workspace-wide builds (`cargo test --workspace --lib`) compile these kernels once, then cache. That trade was made in #550 (glm52) and extended to kimi-k2 / deepseek-v2-lite. +- Corollary: without `required-features`, a bare `cargo test --workspace` (no `--lib`) also builds and *runs* Tier A GPU gates, which fail loudly on hosts missing the weights or GPU count — same as the qwen3/glm52 gates today. The routine sweep is `--lib`; gates run per package. + +### 4. Model crates — Tier B (build-time Python toolchain): one whole-crate gate + +Applies to: **qwen35-4b** (Triton), **deepseek-v4** (TileLang + CuTe DSL). + +- Keep the self-named feature, forwarding to the kernels feature: `deepseek-v4 = ["openinfer-kernels/deepseek-v4"]`. +- The *only* in-crate gate is `#![cfg(feature = "")]` at the top of `lib.rs`. With the feature off, the crate compiles to nothing. Scattered item-level `#[cfg]` gates and reject-only stubs are forbidden — they rot into the load-only scaffold #550 had to remove. +- Tests/bins/benches declare `required-features = [""]`. + +### 5. `openinfer-server` — the selection point + +- Tier A: ` = ["dep:openinfer-"]`. +- Tier B: ` = ["dep:openinfer-", "openinfer-/"]`. +- `default = ["qwen3"]` keeps the stock build pure Rust + CUDA. +- Server code touches a model crate only under `#[cfg(feature = "")]`; the "rebuild with --features" error message lives in the server dispatch (`server_engine.rs`), not in model crates. + +## What this means in practice + +| Command | Features in play | +| --- | --- | +| `cargo run --release -- --model-path …` | server `qwen3` (default) | +| `cargo run --release --features kimi-k2 -- …` | server feature; the kimi crate needs no flag of its own | +| `cargo test --release -p openinfer-kimi-k2` | none needed — Tier A crates build whole | +| `cargo test --release -p openinfer-qwen35-4b --features qwen35-4b --test hf_golden_gate` | Tier B: the self feature is required | +| `cargo test --release --workspace --lib` | compiles Tier A kernels (incl. `moe` substrate → needs NCCL), skips Tier B (no Python required) | + +Feature unification note: cargo compiles `openinfer-kernels` once per distinct feature set in a build graph. Default-member builds (server + qwen3) and `--workspace` builds are two different sets; both stay cached side by side in `target/`, so switching between them does not thrash nvcc after the first build of each. + +## Adding a new model line + +1. Add the kernels feature (` = []` or `= ["moe"]`) and gate its `csrc//` sources in `build.rs`. +2. Decide the tier by one question: *does building the kernels need anything beyond nvcc?* No → Tier A. Yes → Tier B, and document the toolchain requirement next to the feature declaration. +3. Wire the server feature per rule 5 and add the `#[cfg]` arms in `server_engine.rs` / `main.rs`. +4. Bring-up scaffolding (load-only binaries, reject-only coordinators) is fine on a branch but must not merge behind a crate-internal feature — see #550 for the cleanup that pattern forces later. diff --git a/docs/index.md b/docs/index.md index 7290f121..f3e819df 100644 --- a/docs/index.md +++ b/docs/index.md @@ -202,3 +202,4 @@ Organized by domain (model line / subsystem / playbook / lesson) instead of by l | --- | --- | | `conventions/bench-regression.md` | Benchmark regression tracking: one snapshot per model, git-tracked history, TPOT >2% / TTFT >3% thresholds. | | `conventions/coding-style.md` | Testing principle: prefer integration tests, don't test what E2E catches. | +| `conventions/feature-flags.md` | Cargo feature standard: one name per model line; pure-CUDA model crates carry no self feature (Tier A), Python-toolchain crates keep a single whole-crate gate (Tier B); selection lives in server + kernels. | diff --git a/docs/models/deepseek-v2-lite/decode-attribution-gate.md b/docs/models/deepseek-v2-lite/decode-attribution-gate.md index 0c4ba92b..08dc3311 100644 --- a/docs/models/deepseek-v2-lite/decode-attribution-gate.md +++ b/docs/models/deepseek-v2-lite/decode-attribution-gate.md @@ -39,14 +39,14 @@ OPENINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite \ OPENINFER_DSV2_LITE_E2E_CASE_SET=test_data/deepseek-v2-lite-ep2-cases.json \ OPENINFER_DSV2_LITE_E2E_JSON_OUT=target/accuracy/dsv2-lite-ep2/host-staged.json \ cargo test --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite --test e2e_ep2 -- --nocapture + --test e2e_ep2 -- --nocapture OPENINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite \ OPENINFER_DSV2_LITE_E2E_CASE_SET=test_data/deepseek-v2-lite-ep2-cases.json \ OPENINFER_DSV2_LITE_EP_BACKEND=nccl \ OPENINFER_DSV2_LITE_E2E_JSON_OUT=target/accuracy/dsv2-lite-ep2/nccl.json \ cargo test --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite --test e2e_ep2 -- --nocapture + --test e2e_ep2 -- --nocapture python tools/accuracy/compare_dsv2_lite_ep2_outputs.py \ --hf target/accuracy/dsv2-lite-ep2/hf.json \ @@ -60,7 +60,6 @@ Collect batch-1 attribution: ```bash cargo run --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite \ --bin dsv2_lite_ep2_decode_attribution \ -- --model-path models/DeepSeek-V2-Lite \ --batch-size 1 \ @@ -68,7 +67,6 @@ cargo run --release -p openinfer-deepseek-v2-lite \ OPENINFER_DSV2_LITE_EP_BACKEND=nccl \ cargo run --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite \ --bin dsv2_lite_ep2_decode_attribution \ -- --model-path models/DeepSeek-V2-Lite \ --batch-size 1 \ @@ -80,7 +78,6 @@ Run the full decode graph probe: ```bash OPENINFER_DSV2_LITE_EP_BACKEND=nccl \ cargo run --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite \ --bin dsv2_lite_ep2_decode_attribution \ -- --model-path models/DeepSeek-V2-Lite \ --batch-size 1 \ diff --git a/docs/models/deepseek-v2-lite/device-resident-nccl-combine.md b/docs/models/deepseek-v2-lite/device-resident-nccl-combine.md index c2cd140a..1f9c3e57 100644 --- a/docs/models/deepseek-v2-lite/device-resident-nccl-combine.md +++ b/docs/models/deepseek-v2-lite/device-resident-nccl-combine.md @@ -37,10 +37,10 @@ Validated 2026-06-08 on the provided 2x RTX 5090 host with DeepSeek-V2-Lite snap Commands run: ```bash -cargo test --offline --release -p openinfer-deepseek-v2-lite --features deepseek-v2-lite --test e2e_ep2 --no-run +cargo test --offline --release -p openinfer-deepseek-v2-lite --test e2e_ep2 --no-run cargo clippy --offline --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite --bins --tests -- \ + --bins --tests -- \ -D warnings \ -A clippy::option_option \ -A clippy::manual_midpoint \ @@ -54,12 +54,12 @@ python tools/accuracy/hf_dump_dsv2_lite_ep2_greedy.py \ OPENINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite \ OPENINFER_DSV2_LITE_E2E_JSON_OUT=target/accuracy/dsv2-lite-ep2/host-staged.json \ - cargo test --offline --release -p openinfer-deepseek-v2-lite --features deepseek-v2-lite --test e2e_ep2 -- --nocapture + cargo test --offline --release -p openinfer-deepseek-v2-lite --test e2e_ep2 -- --nocapture OPENINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite \ OPENINFER_DSV2_LITE_EP_BACKEND=nccl \ OPENINFER_DSV2_LITE_E2E_JSON_OUT=target/accuracy/dsv2-lite-ep2/nccl-after-decouple-cleanup.json \ - cargo test --offline --release -p openinfer-deepseek-v2-lite --features deepseek-v2-lite --test e2e_ep2 -- --nocapture + cargo test --offline --release -p openinfer-deepseek-v2-lite --test e2e_ep2 -- --nocapture python tools/accuracy/compare_dsv2_lite_ep2_outputs.py \ --hf target/accuracy/dsv2-lite-ep2/hf.json \ @@ -70,7 +70,6 @@ python tools/accuracy/compare_dsv2_lite_ep2_outputs.py \ OPENINFER_DSV2_LITE_EP_BACKEND=nccl \ cargo run --offline --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite \ --bin dsv2_lite_ep2_decode_attribution \ -- --model-path models/DeepSeek-V2-Lite \ --batch-size 1 \ @@ -92,7 +91,7 @@ Follow-up review gate on 2026-06-09 after fixing those lints: cargo fmt --all --check cargo clippy --release -p openinfer-deepseek-v2-lite \ - --features deepseek-v2-lite --bins --tests -- -D warnings + --bins --tests -- -D warnings ``` Both commands passed on the same remote source copy after syncing the follow-up `host_ops.rs` and `logging.rs` fixes. The clippy command ran without `clippy::manual_midpoint`, `clippy::needless_range_loop`, or `clippy::option_option` allows. diff --git a/docs/models/deepseek-v2-lite/hf-accuracy-gate.md b/docs/models/deepseek-v2-lite/hf-accuracy-gate.md index 30d8b9c1..df0c8e37 100644 --- a/docs/models/deepseek-v2-lite/hf-accuracy-gate.md +++ b/docs/models/deepseek-v2-lite/hf-accuracy-gate.md @@ -59,13 +59,13 @@ python tools/accuracy/hf_dump_dsv2_lite_ep2_greedy.py \ OPENINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite \ OPENINFER_DSV2_LITE_E2E_CASE_SET=test_data/deepseek-v2-lite-ep2-cases.json \ OPENINFER_DSV2_LITE_E2E_JSON_OUT=target/accuracy/dsv2-lite-ep2/host-staged.json \ - cargo test --release -p openinfer-deepseek-v2-lite --features deepseek-v2-lite --test e2e_ep2 -- --nocapture + cargo test --release -p openinfer-deepseek-v2-lite --test e2e_ep2 -- --nocapture OPENINFER_TEST_MODEL_PATH=models/DeepSeek-V2-Lite \ OPENINFER_DSV2_LITE_E2E_CASE_SET=test_data/deepseek-v2-lite-ep2-cases.json \ OPENINFER_DSV2_LITE_EP_BACKEND=nccl \ OPENINFER_DSV2_LITE_E2E_JSON_OUT=target/accuracy/dsv2-lite-ep2/nccl.json \ - cargo test --release -p openinfer-deepseek-v2-lite --features deepseek-v2-lite --test e2e_ep2 -- --nocapture + cargo test --release -p openinfer-deepseek-v2-lite --test e2e_ep2 -- --nocapture python tools/accuracy/compare_dsv2_lite_ep2_outputs.py \ --hf target/accuracy/dsv2-lite-ep2/hf.json \ diff --git a/docs/models/deepseek-v2-lite/source-layout.md b/docs/models/deepseek-v2-lite/source-layout.md index 65fb2f91..44bf8364 100644 --- a/docs/models/deepseek-v2-lite/source-layout.md +++ b/docs/models/deepseek-v2-lite/source-layout.md @@ -66,7 +66,7 @@ Remote validation ran on Ubuntu 22.04 with 2x NVIDIA GeForce RTX 5090, driver Passed gates: -- `cargo check --offline --release -p openinfer-deepseek-v2-lite --features deepseek-v2-lite --lib --tests` +- `cargo check --offline --release -p openinfer-deepseek-v2-lite --lib --tests` - HF oracle dump with `tools/accuracy/hf_dump_dsv2_lite_ep2_greedy.py` - host-staged `tests/e2e_ep2.rs` - NCCL `tests/e2e_ep2.rs` using `LD_LIBRARY_PATH=/root/autodl-tmp/nccl-2.27.7/nvidia/nccl/lib` diff --git a/docs/models/kimi-k2/accuracy-gate.md b/docs/models/kimi-k2/accuracy-gate.md index 9fd63164..998c702c 100644 --- a/docs/models/kimi-k2/accuracy-gate.md +++ b/docs/models/kimi-k2/accuracy-gate.md @@ -63,7 +63,7 @@ GPU-argmax-vs-host-log-softmax disagreement on the same logits. # Run the gate (8 GPUs; vLLM must be stopped first — both need the full node): OPENINFER_TEST_MODEL_PATH=/data/models/Kimi-K2.6 \ -cargo test -p openinfer-kimi-k2 --features kimi-k2 --release \ +cargo test -p openinfer-kimi-k2 --release \ --test vllm_golden_gate -- --nocapture ``` diff --git a/openinfer-deepseek-v2-lite/Cargo.toml b/openinfer-deepseek-v2-lite/Cargo.toml index 242545bb..8b06001e 100644 --- a/openinfer-deepseek-v2-lite/Cargo.toml +++ b/openinfer-deepseek-v2-lite/Cargo.toml @@ -7,10 +7,6 @@ autobenches = false autobins = false autotests = false -[features] -default = [] -deepseek-v2-lite = ["dep:openinfer-kernels", "openinfer-kernels/deepseek-v2-lite"] - [dependencies] anyhow = { workspace = true } cudarc = { workspace = true } @@ -21,7 +17,7 @@ log = { workspace = true } memmap2 = { workspace = true } nvtx = { workspace = true } openinfer-core = { workspace = true } -openinfer-kernels = { workspace = true, optional = true } +openinfer-kernels = { workspace = true, features = ["deepseek-v2-lite"] } openinfer-engine = { workspace = true } safetensors = { workspace = true } serde = { workspace = true } @@ -36,9 +32,7 @@ workspace = true [[test]] name = "e2e_ep2" path = "tests/e2e_ep2.rs" -required-features = ["deepseek-v2-lite"] [[bin]] name = "dsv2_lite_ep2_decode_attribution" path = "src/bin/dsv2_lite_ep2_decode_attribution.rs" -required-features = ["deepseek-v2-lite"] diff --git a/openinfer-deepseek-v2-lite/src/lib.rs b/openinfer-deepseek-v2-lite/src/lib.rs index 6a922449..b1ef8837 100644 --- a/openinfer-deepseek-v2-lite/src/lib.rs +++ b/openinfer-deepseek-v2-lite/src/lib.rs @@ -1,37 +1,25 @@ -#[cfg(feature = "deepseek-v2-lite")] mod attribution; mod config; -#[cfg(feature = "deepseek-v2-lite")] mod device; -#[cfg(feature = "deepseek-v2-lite")] mod engine; mod ep; -#[cfg(feature = "deepseek-v2-lite")] mod host_ops; -#[cfg(feature = "deepseek-v2-lite")] mod model; -#[cfg(feature = "deepseek-v2-lite")] mod nccl_backend; -#[cfg(feature = "deepseek-v2-lite")] mod runtime; -#[cfg(feature = "deepseek-v2-lite")] mod scheduler; -#[cfg(feature = "deepseek-v2-lite")] mod weights; use std::path::Path; use anyhow::Result; -#[cfg(feature = "deepseek-v2-lite")] use openinfer_engine::engine::EpBackend; use openinfer_engine::engine::{EngineHandle, EngineLoadOptions}; -#[cfg(feature = "deepseek-v2-lite")] pub use attribution::{CallSiteRollup, DecodeAttributionProfile, SectionRollup, SectionSample}; pub use config::Config; use config::SUPPORTED_HIDDEN_SIZE; use ep::SUPPORTED_ROUTED_EXPERTS; -#[cfg(feature = "deepseek-v2-lite")] pub use runtime::{ BatchedGenerationResult, DecodeGraphReadinessReport, DeepSeekV2LiteEp2Generator, GenerationResult, GenerationStats, @@ -62,25 +50,16 @@ pub fn probe_config_json(json: &serde_json::Value) -> Result { Ok(true) } -#[cfg(feature = "deepseek-v2-lite")] pub fn start_engine(model_path: &Path, options: EngineLoadOptions) -> Result { engine::start_engine(model_path, options) } -#[cfg(not(feature = "deepseek-v2-lite"))] -pub fn start_engine(_model_path: &Path, _options: EngineLoadOptions) -> Result { - anyhow::bail!( - "DeepSeek-V2-Lite runtime is feature-gated; rebuild with --features deepseek-v2-lite" - ) -} - /// Start the DeepSeek-V2-Lite engine for the server. The binary forwards the /// user's `cuda_graph` request uniformly; whether to honor it is the model's /// call. The server EP=2 path does not enable CUDA Graph capture, so it ignores /// the request (warning if one came in). The diagnostic decode graph probe lives /// in the attribution gate. The EP=2 topology (devices `0..1`) is fixed by the /// model. -#[cfg(feature = "deepseek-v2-lite")] pub fn launch(model_path: &Path, cuda_graph: bool) -> Result { if cuda_graph { log::warn!("DeepSeek V2 Lite does not support CUDA Graph; ignoring --cuda-graph=true"); @@ -97,10 +76,3 @@ pub fn launch(model_path: &Path, cuda_graph: bool) -> Result { }, ) } - -#[cfg(not(feature = "deepseek-v2-lite"))] -pub fn launch(_model_path: &Path, _cuda_graph: bool) -> Result { - anyhow::bail!( - "DeepSeek-V2-Lite runtime is feature-gated; rebuild with --features deepseek-v2-lite" - ) -} diff --git a/openinfer-deepseek-v4/src/lib.rs b/openinfer-deepseek-v4/src/lib.rs index 68540740..c40c8217 100644 --- a/openinfer-deepseek-v4/src/lib.rs +++ b/openinfer-deepseek-v4/src/lib.rs @@ -1,17 +1,16 @@ +//! DeepSeek-V4 model crate. The whole crate rides the `deepseek-v4` feature: +//! its kernels need TileLang + CuTe DSL Python toolchains at build time, so a +//! featureless workspace build must not pull them in. +#![cfg(feature = "deepseek-v4")] + mod config; -#[cfg(feature = "deepseek-v4")] mod direct; -#[cfg(feature = "deepseek-v4")] pub mod e2e_runner; -#[cfg(feature = "deepseek-v4")] mod model; -#[cfg(feature = "deepseek-v4")] mod runtime; -#[cfg(feature = "deepseek-v4")] mod weights; pub use config::{Config, RopeScaling, TensorParallelConfig}; -#[cfg(feature = "deepseek-v4")] pub use direct::{ DeepSeekV4DirectGenerator, DeepSeekV4RequestState, DirectDecodeStep, DirectGeneration, DirectKvCacheActiveSnapshot, DirectKvCacheLease, DirectKvCacheReject, @@ -23,7 +22,6 @@ pub use direct::{ /// the model's call, not the server's. V4's direct engine has no CUDA Graph /// capture, so it ignores the request (warning if one came in). The MP8 /// topology (devices `0..7`) is likewise fixed by the model. -#[cfg(feature = "deepseek-v4")] pub fn launch( model_path: &std::path::Path, cuda_graph: bool, @@ -48,14 +46,12 @@ pub fn launch( }, ) } -#[cfg(feature = "deepseek-v4")] pub use model::{ AttentionWeightNames, AttentionWeights, BlockWeightNames, BlockWeights, CompressorWeightNames, CompressorWeights, DeepSeekRankModel, ExpertWeightNames, ExpertWeights, FfnWeightNames, FfnWeights, IndexerWeightNames, IndexerWeights, QuantLinearNames, QuantLinearRef, RankWeightView, TensorRef, TopLevelWeightNames, }; -#[cfg(feature = "deepseek-v4")] pub use runtime::{ AttentionProjections, Bf16Cache, Bf16HiddenStates, CompressorDecodeState, DeepSeekRopeCache, F32HiddenStates, F32Logits, HcHiddenStates, HcPreState, LayerDecodeCache, MoeFusedRoutePlan, @@ -88,7 +84,6 @@ pub use runtime::{ sparse_attention_prefill_bf16_hidden, window_and_compress_topk_indices, window_topk_indices, window_topk_indices_decode, }; -#[cfg(feature = "deepseek-v4")] pub use weights::{ GpuRawTensor, RankGpuContext, RankManifest, RankWeights, TensorInfo, load_rank_manifest, load_rank_subset_to_gpu, load_rank_to_gpu, mp_rank_path, diff --git a/openinfer-kernels/Cargo.toml b/openinfer-kernels/Cargo.toml index eb3b4e60..ee026b5a 100644 --- a/openinfer-kernels/Cargo.toml +++ b/openinfer-kernels/Cargo.toml @@ -17,15 +17,19 @@ cc = { workspace = true } openinfer-build = { workspace = true } [features] +# One feature per model line scoping its csrc/ + AOT codegen; features that +# need more than nvcc say so here. See docs/conventions/feature-flags.md. default = [] tvm-ffi-triton-cubin = ["dep:tvm-ffi", "qwen35-4b"] -# Qwen3.5 Triton AOT kernels (GDR chunkwise prefill) — the only feature that -# needs Python + Triton at build time. +# Qwen3.5 Triton AOT kernels (GDR chunkwise prefill) — needs Python + Triton +# at build time. qwen35-4b = [] +# DeepSeek-V4 kernels — need Python + TileLang + CuTe DSL at build time. deepseek-v4 = [] deepseek-v4-cutedsl-diagnostic = ["deepseek-v4"] deepseek-v2-lite = [] # Shared MoE/MLA third-party substrate: DeepEP, DeepGEMM, and FlashMLA. +# Needs NCCL >= 2.30.4 headers/lib for the DeepEP shim. moe = [] glm52 = ["moe"] kimi-k2 = ["moe"] diff --git a/openinfer-kimi-k2/Cargo.toml b/openinfer-kimi-k2/Cargo.toml index 38487f23..9b284d5c 100644 --- a/openinfer-kimi-k2/Cargo.toml +++ b/openinfer-kimi-k2/Cargo.toml @@ -18,7 +18,7 @@ log = { workspace = true } memmap2 = { workspace = true } openinfer-bench = { workspace = true } openinfer-core = { workspace = true } -openinfer-kernels = { workspace = true } +openinfer-kernels = { workspace = true, features = ["kimi-k2"] } openinfer-sample = { workspace = true } openinfer-kv-cache = { workspace = true } rand = { workspace = true } @@ -29,18 +29,15 @@ tokio = { workspace = true } [features] default = [] -kimi-k2 = ["openinfer-kernels/kimi-k2"] kernel-call-trace = ["openinfer-core/kernel-call-trace"] kernel-report = [ "dep:clap", - "kimi-k2", "kernel-call-trace", ] [[test]] name = "vllm_golden_gate" path = "tests/vllm_golden_gate.rs" -required-features = ["kimi-k2"] [[bin]] name = "kimi_kernel_report" diff --git a/openinfer-kimi-k2/src/config.rs b/openinfer-kimi-k2/src/config.rs index 8971c805..5a2b53fb 100644 --- a/openinfer-kimi-k2/src/config.rs +++ b/openinfer-kimi-k2/src/config.rs @@ -1,9 +1,5 @@ //! Kimi-K2.6 text-only constants, config probing, and derived shapes. -// Most consumers live behind the kimi-k2 feature; the constants and shape -// helpers are intentionally kept compiling in feature-off builds. -#![cfg_attr(not(feature = "kimi-k2"), allow(dead_code))] - use std::{fs, path::Path}; use anyhow::{Context, Result, bail, ensure}; diff --git a/openinfer-kimi-k2/src/lib.rs b/openinfer-kimi-k2/src/lib.rs index 461ee2be..9796ef7f 100644 --- a/openinfer-kimi-k2/src/lib.rs +++ b/openinfer-kimi-k2/src/lib.rs @@ -1,7 +1,4 @@ //! Text-only Kimi-K2.6 model crate. -//! -//! The current crate stage owns the compile-checked operator API surface and -//! text-only config probing. CUDA/runtime bodies land behind these headers. #![allow(incomplete_features)] // `use super::*` is the flat-module-layout idiom for these tightly-coupled @@ -14,40 +11,28 @@ use std::path::Path; use anyhow::Result; -#[cfg(feature = "kimi-k2")] use openinfer_core::engine::EpBackend; use openinfer_core::engine::{EngineHandle, EngineLoadOptions}; -#[cfg(feature = "kimi-k2")] pub mod batch_decode_trace; pub(crate) mod config; #[cfg(feature = "kernel-report")] pub mod kernel_report; -#[cfg(feature = "kimi-k2")] mod runner; -#[cfg(feature = "kimi-k2")] mod typed_scratch; -#[cfg(feature = "kimi-k2")] mod weights; pub use config::{KIMI_K2_LAYERS, probe_config_json}; -#[cfg(feature = "kimi-k2")] #[allow(clippy::needless_pass_by_value)] pub fn start_engine(model_path: &Path, options: EngineLoadOptions) -> Result { runner::start_engine(model_path, &options) } -#[cfg(not(feature = "kimi-k2"))] -pub fn start_engine(_model_path: &Path, _options: EngineLoadOptions) -> Result { - anyhow::bail!("Kimi-K2 runtime is feature-gated; rebuild with --features kimi-k2") -} - /// Server-facing launch knobs for Kimi-K2. The binary passes raw CLI values; /// [`launch`] owns the EP topology policy — validating TP/DP, deriving the EP /// world and its device ordinals — so the server never hardcodes Kimi's /// parallel layout. -#[cfg(feature = "kimi-k2")] #[derive(Clone, Copy, Debug)] pub struct KimiLaunchOptions { pub tp_size: usize, @@ -57,7 +42,6 @@ pub struct KimiLaunchOptions { } /// Start the Kimi-K2 engine from server-facing [`KimiLaunchOptions`]. -#[cfg(feature = "kimi-k2")] pub fn launch(model_path: &Path, options: KimiLaunchOptions) -> Result { use log::info; use openinfer_core::parallel::ParallelConfig; diff --git a/openinfer-kimi-k2/tests/vllm_golden_gate.rs b/openinfer-kimi-k2/tests/vllm_golden_gate.rs index 74f2b9cd..d50f7c61 100644 --- a/openinfer-kimi-k2/tests/vllm_golden_gate.rs +++ b/openinfer-kimi-k2/tests/vllm_golden_gate.rs @@ -47,9 +47,10 @@ //! Requires 8 GPUs and Kimi-K2.6 weights. `OPENINFER_TEST_MODEL_PATH` must //! point at the weights and the fixture must exist — both fail loudly when //! missing. No silent skip: a gate that can quietly report "ok 0.00s" guards -//! nothing (the qwen35 gate's env-gated skip taught us that). Building the -//! target at all requires the `kimi-k2` feature (`required-features` in -//! Cargo.toml), so feature-less workspace test runs never see it. +//! nothing (the qwen35 gate's env-gated skip taught us that). Like the qwen3 +//! and glm52 gates, this target builds in any workspace test sweep and fails +//! loudly on hosts without the weights — run gates per package, and keep the +//! routine sweep to `cargo test --workspace --lib`. use std::path::Path; use std::time::{Duration, Instant}; diff --git a/openinfer-server/Cargo.toml b/openinfer-server/Cargo.toml index 75f7bab4..68795059 100644 --- a/openinfer-server/Cargo.toml +++ b/openinfer-server/Cargo.toml @@ -48,15 +48,20 @@ vllm-text = { workspace = true } tikv-jemallocator = "0.6" [features] +# One feature per model line, same name as the crate suffix. Pure-CUDA model +# crates compile their full selves; the server feature just adds the dep. +# Toolchain-gated crates (build-time Python) additionally forward their +# self-named feature. See docs/conventions/feature-flags.md. # Qwen3-4B is the default model line: pure Rust + CUDA, no Python at build time. default = ["qwen3"] qwen3 = ["dep:openinfer-qwen3"] # Qwen3.5 pulls in the Triton AOT kernels (Python + Triton at build time). qwen35-4b = ["dep:openinfer-qwen35-4b", "openinfer-qwen35-4b/qwen35-4b"] +# DeepSeek-V4 pulls in TileLang + CuTe DSL AOT kernels (Python at build time). deepseek-v4 = ["dep:openinfer-deepseek-v4", "openinfer-deepseek-v4/deepseek-v4"] -deepseek-v2-lite = ["dep:openinfer-deepseek-v2-lite", "openinfer-deepseek-v2-lite/deepseek-v2-lite"] +deepseek-v2-lite = ["dep:openinfer-deepseek-v2-lite"] glm52 = ["dep:openinfer-glm52"] -kimi-k2 = ["dep:openinfer-kimi-k2", "openinfer-kimi-k2/kimi-k2"] +kimi-k2 = ["dep:openinfer-kimi-k2"] pplx-ep = ["deepseek-v4", "openinfer-deepseek-v4/pplx-ep"] deepseek-v4-cutedsl-diagnostic = [ "deepseek-v4", diff --git a/scripts/bench_dsv2lite_vllm_matrix.py b/scripts/bench_dsv2lite_vllm_matrix.py index 798ee0db..f3a60c42 100644 --- a/scripts/bench_dsv2lite_vllm_matrix.py +++ b/scripts/bench_dsv2lite_vllm_matrix.py @@ -386,7 +386,7 @@ def run_correctness_gate(args: argparse.Namespace, out_dir: Path) -> dict[str, A ), ( ["cargo", "test", "--release", "-p", "openinfer-deepseek-v2-lite", - "--features", "deepseek-v2-lite", "--test", "e2e_ep2", "--", "--nocapture"], + "--test", "e2e_ep2", "--", "--nocapture"], { "OPENINFER_TEST_MODEL_PATH": model, "OPENINFER_DSV2_LITE_E2E_CASE_SET": case_set, @@ -396,7 +396,7 @@ def run_correctness_gate(args: argparse.Namespace, out_dir: Path) -> dict[str, A ), ( ["cargo", "test", "--release", "-p", "openinfer-deepseek-v2-lite", - "--features", "deepseek-v2-lite", "--test", "e2e_ep2", "--", "--nocapture"], + "--test", "e2e_ep2", "--", "--nocapture"], { "OPENINFER_TEST_MODEL_PATH": model, "OPENINFER_DSV2_LITE_E2E_CASE_SET": case_set, @@ -456,7 +456,7 @@ def run_direct_diagnostic(args: argparse.Namespace, out_dir: Path) -> list[dict[ artifact.parent.mkdir(parents=True, exist_ok=True) cmd = [ "cargo", "run", "--release", "-p", "openinfer-deepseek-v2-lite", - "--features", "deepseek-v2-lite", "--bin", "dsv2_lite_ep2_decode_attribution", + "--bin", "dsv2_lite_ep2_decode_attribution", "--", "--model-path", str(args.model_path), "--batch-size", str(batch_size), "--out", display_path(artifact), ] @@ -1536,7 +1536,7 @@ def plan(args: argparse.Namespace) -> dict[str, Any]: "batch_size": batch, "command": redact_command([ "cargo", "run", "--release", "-p", "openinfer-deepseek-v2-lite", - "--features", "deepseek-v2-lite", "--bin", "dsv2_lite_ep2_decode_attribution", + "--bin", "dsv2_lite_ep2_decode_attribution", "--", "--model-path", str(args.model_path), "--batch-size", str(batch), ]), } From ac628de267da2724bea12eb2b09a48fc0ff74de8 Mon Sep 17 00:00:00 2001 From: xiaguan <751080330@qq.com> Date: Sat, 4 Jul 2026 05:52:07 -0400 Subject: [PATCH 3/3] chore(tests): adopt cargo-nextest as the test runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sweep contract moves from convention to config: a bare `cargo nextest run --workspace` runs lib unit tests, excludes hardware gates via default-filter, and reports every exclusion as skipped — selection is explicit, never silent. Gates opt in per package with --ignore-default-filter. .config/nextest.toml encodes: - default-filter = lib tests minus the one RDMA-hardware lib test (gdr_copy_flag_GPU), so the sweep is green on plain GPU boxes where cargo test --lib was red - retries = 0: flaky = bug; exact-match gates must not rot under retry - slow-timeout with terminate-after: a wedged NCCL collective goes red instead of hanging the session - gpu-gate (serial) / gpu-lib (max 4) groups: nextest is process-per-test, engines and CUDA contexts must not stack nextest also compiles every test target where --lib compiles none, which immediately surfaced two rotted files, fixed here: - kv-offload cpu_roundtrip.rs still used the pre-#522 QueryOutcome struct fields - kernels glm52_indexer_smoke.rs had an unused variable Measured on the 5070 Ti box: sweep 725 passed / 12 skipped in ~10s (cargo test --lib --no-fail-fast: 4.2s but red on gdr); qwen3 hf_golden_gate and qwen35-4b e2e_scheduler both pass through the documented nextest invocations. Requires nextest >= 0.9.138 (0.9.70 fails to list proc-macro test binaries: dynamic libstd). Co-Authored-By: Claude Fable 5 --- .config/nextest.toml | 57 +++++++++++++++++++ CLAUDE.md | 17 +++--- docs/conventions/coding-style.md | 18 ++++++ docs/index.md | 2 +- .../tests/glm52_indexer_smoke.rs | 1 - openinfer-kv-offload/tests/cpu_roundtrip.rs | 7 ++- 6 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 .config/nextest.toml diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 00000000..cc85a013 --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,57 @@ +# Test runner config. Rationale in docs/conventions/coding-style.md. +# +# The contract: a bare `cargo nextest run --workspace` is the sweep — lib unit +# tests only, safe on any single-GPU box. Hardware gates (integration tests +# needing model weights / multi-GPU) are excluded by the default filter and +# reported as skipped; run them per package with --ignore-default-filter. + +[profile.default] +# Sweep = lib tests, minus the one lib test that needs RDMA hardware: +# gdr_copy_flag_GPU requires a GDRCopy handle (gdrdrv), absent on plain GPU +# workstations. It is excluded here — not env-probed inside the test — so the +# selection stays visible; run it explicitly on RDMA hosts with +# `cargo nextest run -p openinfer-comm-cuda-lib --ignore-default-filter`. +default-filter = "kind(lib) - (package(openinfer-comm-cuda-lib) & test(gdr_copy_flag_GPU))" +# Flaky = bug. Exact-match greedy gates are this repo's core asset; retries +# would let nondeterminism rot unnoticed. +retries = 0 +# A wedged NCCL collective or CUDA deadlock must go red, not hang the session. +slow-timeout = { period = "60s", terminate-after = 5 } + +# Integration gates load real model weights — legitimately slow. Mark slow at +# 5 min, kill at 1 h (a gate that quiet for an hour is hung, not loading). +[[profile.default.overrides]] +filter = "kind(test)" +slow-timeout = { period = "300s", terminate-after = 12 } + +# nextest is process-per-test, so parallel GPU tests each create their own +# CUDA context on the same device. Two tiers: +# gpu-gate: integration gates load full engines (GBs of VRAM, NCCL ports) — +# strictly one at a time. +# gpu-lib: CUDA-touching lib tests hold small contexts — bounded, not +# serial, so pure-logic lib tests in the same packages don't queue +# behind a single slot. +# CPU-only suites (kv-cache, sim, vllm-support, ...) keep full parallelism. +[test-groups] +gpu-gate = { max-threads = 1 } +gpu-lib = { max-threads = 4 } + +[[profile.default.overrides]] +filter = "kind(test)" +test-group = "gpu-gate" + +[[profile.default.overrides]] +filter = """ +kind(lib) & ( + package(openinfer-core) + | package(openinfer-kernels) + | package(openinfer-sample) + | package(openinfer-qwen3) + | package(openinfer-qwen35-4b) + | package(openinfer-glm52) + | package(openinfer-kimi-k2) + | package(openinfer-deepseek-v2-lite) + | package(openinfer-deepseek-v4) +) +""" +test-group = "gpu-lib" diff --git a/CLAUDE.md b/CLAUDE.md index 5763aeeb..f2e41600 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,17 +47,20 @@ cargo run --release --features glm52 -- --model-path models/GLM5.2 ## Tests +The runner is cargo-nextest (`cargo binstall cargo-nextest`, >= 0.9.138; config and rationale in `.config/nextest.toml` + `docs/conventions/coding-style.md`). Bare `cargo test` still works, but nextest owns the sweep contract: hardware gates are excluded by the config's default filter and reported as skipped, and hung GPU tests are killed instead of wedging the session. + ```bash -# Unit tests (~9s) -cargo test --release --workspace --lib +# Sweep (~10s): lib unit tests, green on any single-GPU box +cargo nextest run --release --workspace -# Accuracy and integration tests — require GPU + model weights -cargo test --release -p openinfer-qwen3 --test hf_golden_gate -OPENINFER_TEST_MODEL_PATH=models/Qwen3.5-4B cargo test --release -p openinfer-qwen35-4b --features qwen35-4b --test hf_golden_gate -OPENINFER_TEST_MODEL_PATH=models/Qwen3.5-4B cargo test --release -p openinfer-qwen35-4b --features qwen35-4b --test e2e_scheduler +# Accuracy and integration gates — require GPU + model weights, opt in per package +OPENINFER_TEST_MODEL_PATH=models/Qwen3-4B \ + cargo nextest run --release -p openinfer-qwen3 --ignore-default-filter -E 'binary(hf_golden_gate)' +OPENINFER_TEST_MODEL_PATH=models/Qwen3.5-4B \ + cargo nextest run --release -p openinfer-qwen35-4b --features qwen35-4b --ignore-default-filter -E 'binary(e2e_scheduler)' # Single test (filter by name) -cargo test --release --workspace --lib prefix_cache -- --nocapture +cargo nextest run --release --workspace -E 'test(prefix_cache)' ``` Qwen accuracy gates compare logits against stored HF golden fixtures. Qwen3.5 exact-text JSON baselines are retired; keep `e2e_scheduler` for scheduler liveness and request-flow coverage. diff --git a/docs/conventions/coding-style.md b/docs/conventions/coding-style.md index 0eff624b..081ca27e 100644 --- a/docs/conventions/coding-style.md +++ b/docs/conventions/coding-style.md @@ -4,6 +4,24 @@ Don't test for the sake of testing. Prefer integration tests over unit tests — if the E2E test catches it, a unit test is ceremony. Unit tests earn their place for silent failures: GPU kernels, tricky data-structure invariants, edge-case-rich pure logic. +## Test runner + +`cargo nextest run --workspace` (config in `.config/nextest.toml`) is the sweep: it runs lib unit tests, excludes hardware gates via the config's `default-filter`, and *reports* everything it excluded as skipped — selection is explicit, never silent. Hardware gates run per package with `--ignore-default-filter`: + +```bash +OPENINFER_TEST_MODEL_PATH=/path/to/Qwen3-4B \ + cargo nextest run --release -p openinfer-qwen3 --ignore-default-filter -E 'binary(hf_golden_gate)' +``` + +Why nextest over bare `cargo test`: process-per-test isolation (thread-local cuBLAS/CUDA-context poisoning can't leak between tests — see `lessons/exact-match-gate-thread-cublas.md`), hang-kill via `slow-timeout` (a wedged NCCL collective goes red instead of hanging the session), and GPU concurrency groups (gates strictly serial, CUDA-touching lib tests bounded). It also *compiles* every test target where `cargo test --lib` compiles none of the integration tests — that alone caught a gate that had silently rotted against a renamed API. + +Two rules encoded in the config that must not regress: + +- `retries = 0`. Flaky = bug. Exact-match greedy gates are this repo's core asset; a retry culture rots them. +- No env-probing skips inside tests. If a test can't run on a machine class, exclude it in `default-filter` with a comment (visible in config + counted as skipped) — never auto-skip from inside the test, which reports a green that guards nothing. + +nextest does not run doctests. The workspace has 3 runnable ones today; `cargo test --release --doc --workspace` covers them when doc examples change. + ## Logging Log through `openinfer-core::logging`. The text layout already prints each record's module target, so don't prefix messages with a module or model name — no `kimi-k2:`, no `Qwen3.5 `. Error messages in `anyhow!` / `bail!` keep their prefix; they surface to callers without a target. diff --git a/docs/index.md b/docs/index.md index f3e819df..f7313bbc 100644 --- a/docs/index.md +++ b/docs/index.md @@ -201,5 +201,5 @@ Organized by domain (model line / subsystem / playbook / lesson) instead of by l | Path | TL;DR | | --- | --- | | `conventions/bench-regression.md` | Benchmark regression tracking: one snapshot per model, git-tracked history, TPOT >2% / TTFT >3% thresholds. | -| `conventions/coding-style.md` | Testing principle: prefer integration tests, don't test what E2E catches. | +| `conventions/coding-style.md` | Testing principle (prefer integration tests) + nextest runner contract: sweep vs per-package gates, retries=0, no env-probing skips. | | `conventions/feature-flags.md` | Cargo feature standard: one name per model line; pure-CUDA model crates carry no self feature (Tier A), Python-toolchain crates keep a single whole-crate gate (Tier B); selection lives in server + kernels. | diff --git a/openinfer-kernels/tests/glm52_indexer_smoke.rs b/openinfer-kernels/tests/glm52_indexer_smoke.rs index bb44b8ab..996a1c7a 100644 --- a/openinfer-kernels/tests/glm52_indexer_smoke.rs +++ b/openinfer-kernels/tests/glm52_indexer_smoke.rs @@ -438,7 +438,6 @@ fn deepgemm_paged_mqa_launch() -> Result<()> { let logits = DeviceBuf::zeroed(logits_bytes)?; // block_table: [batch_size, block_table_stride] i32 - let bt_bytes = (batch_size * block_table_stride) as usize * std::mem::size_of::(); let block_table_host: Vec = (0..num_kv_blocks).collect(); let block_table = DeviceBuf::from_host(&block_table_host)?; diff --git a/openinfer-kv-offload/tests/cpu_roundtrip.rs b/openinfer-kv-offload/tests/cpu_roundtrip.rs index 97eea105..849b1707 100644 --- a/openinfer-kv-offload/tests/cpu_roundtrip.rs +++ b/openinfer-kv-offload/tests/cpu_roundtrip.rs @@ -14,7 +14,7 @@ use cudarc::driver::{CudaContext, result}; use half::bf16; use openinfer_kv_cache::KvBuffer; -use openinfer_kv_offload::{OffloadConfig, OffloadEngine}; +use openinfer_kv_offload::{OffloadConfig, OffloadEngine, QueryOutcome}; const NUM_LAYERS: usize = 4; const NUM_KV_HEADS: usize = 2; @@ -108,7 +108,10 @@ fn gpu_cpu_gpu_roundtrip_preserves_kv_bytes() { engine.flush_saves(); // ── Query the CPU tier: the full 3-block prefix must be resident ── - let hit = engine.query("roundtrip-req", &hashes).expect("query"); + // Host-memory-only setup: no deeper tier, so `Loading` is impossible. + let QueryOutcome::Ready(hit) = engine.query("roundtrip-req", &hashes).expect("query") else { + panic!("host-memory-only query must resolve synchronously"); + }; assert_eq!( hit.num_blocks, 3, "all three saved blocks should hit the CPU tier"