Skip to content

v0.15.0

Latest

Choose a tag to compare

@ohdearquant ohdearquant released this 30 Oct 19:23
· 154 commits to main since this release
198664e

lattice v0.15.0

Minor release. The lattice-inference crate is split into a set of smaller crates, and it stays the facade: every public path it had in 0.14.0 outside the serving stack still resolves, now as a re-export (the serving stack moves to lattice-serve; see Breaking changes). Library crates hold grammar-constrained decoding, weight storage, quantization, the Metal runtime, model files, the host-side compute kernels and the model interface. Each model family moves into its own crate under crates/models/: BERT, EmbeddingGemma 2, Qwen3, Gemma 4, ERNIE 4.5 (with PaddleOCR-VL) and Qwen3.5. The serving stack, the lattice CLI and the Q4, QuaRot and MoE admission tools move to the new lattice-serve crate, so a default lattice-inference build no longer compiles axum or clap. lattice-embed now depends on the model-family crates directly instead of on the facade.

Breaking changes

  • The HTTP serving stack and the lattice CLI move out of lattice-inference into the new lattice-serve crate (#2227). lattice_inference::serve, lattice_inference::serving_provider and the serve feature are removed. Depend on lattice-serve and import the same items from lattice_serve::serve and lattice_serve::serving_provider; the paths below the crate root are unchanged. The undocumented #[doc(hidden)] modules serving_cpu, serving_cpu_host, serving_embeddings, serving_factory, serving_host and serving_worker, which 0.14.0 compiled under serve, move to lattice-serve under the same names. Install the CLI with cargo install lattice-serve.
  • The quantize_q4, quantize_quarot and moe_admission_sim binaries move from lattice-inference to lattice-serve (#2252). Build or install them from lattice-serve.
  • lattice_embed::simd is now the new lattice-simd crate, re-exported under the same path, so every item keeps its name. The prepared quantized-tier functions that return a Result now return lattice_simd::Result with the error lattice_simd::SimdError instead of lattice_embed::Result. SimdError has the same TierMismatch and Internal variants, and lattice_embed::EmbedError implements From<SimdError>, so ? in a function returning lattice_embed::Result still compiles; code that matches the error value names SimdError instead. (#2360)
  • The unused sealed embedding attestation API is removed from lattice-embed: AttestationAlgorithm, CheckpointAttestor, NativeResourceBudget and SupplementaryAttestationEvidence (at the crate root and under service), service::prepared::MAX_SUPPLEMENTARY_ATTESTATION_EVIDENCE_BYTES and service::prepared::MIN_SUPPLEMENTARY_ATTESTATION_EVIDENCE_BYTES, and the EmbedError variants AttestationReportSize and ResourceBudgetOverflow. These 0.14.0 paths no longer resolve (#2344).
  • The unused Qwen3-VL vision scaffold is removed from lattice-inference: vision::config, vision::merger, vision::preprocess, vision::vit, VisionConfig, VisionEncoder and VisionOutput. These 0.14.0 paths no longer resolve. The Qwen3.5 vision encoder in lattice-model-qwen35 is unrelated and unchanged (#2337).
  • The #[doc(hidden)] and documented-unsupported lattice_compute::__internal::x86_gemm_dispatch_enabled is removed.
  • LoraAdapter::save_safetensors in lattice-tune refuses a trained module outside the supported text layout (self_attn, linear_attn and mlp projections), naming the module. Before, such a module was written under self_attn, so a BERT adapter (query, key, value, attn_output, ffn_intermediate, ffn_output) was saved in a layout no PEFT reader places. Files saved by 0.14.0 still load. (#2383)
  • lattice-model-qwen3-embedding reads only top-level config.json fields, so nested keys and string values cannot supply model settings. Malformed or non-object JSON, duplicate recognized top-level keys and out-of-range JSON numbers are refused (#2508).
  • BERT configuration loading now parses fields from the JSON root and refuses malformed JSON, a non-object root or duplicate recognized top-level keys. BertConfig::from_json_str is available from lattice-arch, with the existing model-crate path preserved (#2512).
  • BERT configuration loading refuses zero dimensions, hidden sizes that are not divisible by the attention-head count, and non-finite or non-positive layer-normalization epsilon values, including configurations inferred from safetensors shapes (#2515).
  • No other change breaks a 0.14.0 path at default features. cargo-semver-checks ran against 0.14.0 with default features only (non-default features were not checked) and passes lattice-transport, lattice-inference-core and lattice-tokenizer. It does not follow re-exports of items defined in another crate, so for the crates that now re-export moved items it reports paths as missing that still resolve; every path it flagged was therefore also imported from a downstream crate built against this release. For lattice-fann all 21 flagged paths resolve, re-exported from lattice-lora, and for lattice-tune all 4 do (LoraLayer and apply_lora, also re-exported from lattice-lora). For lattice-embed 50 of 62 resolve through the lattice-simd re-export; the 12 that do not are the attestation items above. For lattice-inference 752 of 980 resolve through the facade's re-exports; the 228 that do not are the serve module's items (207), the vision module (20), and one serving_provider item, all listed above. The crates first published in this release have no baseline.
  • cargo-semver-checks has no check for a changed parameter, return or field type, so the public signatures in the rustdoc output of 0.14.0 and of this release were also compared at default features (lattice-model-ernie45 with the paddleocr-vl and image features that the lattice-inference defaults enable). No item that 0.14 exported changed its signature, including the items that moved to a new crate.

Deprecations

Each of these keeps its 0.14 signature and behaviour through 0.15 and names its replacement in the deprecation note. ADR-103 Amendment 21 schedules the removal of the items that name a model type for 0.16.0.

  • SafetensorsFile::load_bert_weights, load_cross_encoder_weights and load_qwen_weights, and ShardedSafetensors::load_qwen_weights_owned, on lattice-weights: use the model layer's weight_loading functions or the neutral tensor reads (#2286, #2283, #2285).
  • GatedDeltaNetState::new (takes the Qwen3.5 configuration): use GatedDeltaNetState::from_geometry with a GdnGeometry (#2270).
  • LoraHook::validate_against and validate_against_bert: implement LoraHook::validate_geometry against a LoraTargetGeometry (#2271).
  • FlatKVCacheConfig::for_qwen3: use FlatKVCacheConfig::new, which takes the same arguments (#2273).
  • plan_prefix_reuse: use plan_prefix_reuse_for_slot, which also declines an entry retained for another slot (#2318).

Separately, lattice_inference::backward::simd is deprecated in favour of lattice_inference::backward::dispatch and scheduled for removal in 0.16.0. The test-utils-gated compatibility path preserves the current exports; ForcedDispatch::mark_executed now refuses caller-written execution markers, whereas 0.14 accepted a matching marker. (#2364)

New crates

  • lattice-arch holds the model interface: the model configuration parsers, QwenConfig and the Qwen3.5 tensor names (#2228, #2232, published under this name after the rename in #2267); the generation and sampling types, StopReason, stop strings and the decode budget (#2268); the decoder session contract and the shared generation driver, public as of this release (#2269, #2272); the text encoder contract (#2274); the model family registry and the loaded decoder contract (#2275); the shared generation admission and seed helpers (#2295). The contracts are public and may still change in a minor release.
  • lattice-grammar holds grammar-constrained decoding: GBNF parsing, the grammar engine and token masks. Its bench-internals feature carries the profiling items, and the facade's bench-internals forwards to it (#2104, ADR-103 Amendment 2 in #2105).
  • lattice-weights holds weight storage, safetensors and the other checkpoint formats, half-precision decoding, and borrowed tensor views for model-neutral reads (#2111, #2278).
  • lattice-quant holds the quantization primitives, including the QuaRot Hadamard transforms (#2113), the QuaRot conversion (#2233) and model-neutral rotation targets (#2281).
  • lattice-compute holds the host-side compute of the forward passes: RoPE tables, KV caches, LoRA hooks, numerical diagnostics, the CPU forward and attention kernels, pooling, the GatedDeltaNet kernels, the Gemma 4 scaled embedding and logit softcap, and the scalar vision helpers (#2244-#2251, #2258-#2260, #2296). Its kernels take model-neutral inputs: AttentionProjections for attention (#2273), GdnGeometry for GatedDeltaNet (#2270), LoraTargetGeometry for LoRA validation (#2271).
  • lattice-model-io holds what every model uses to find its files: the model cache directory, canonical model names, checksum verification, the cache-miss download behind the download feature (refused under LATTICE_OFFLINE), the boolean engine-switch reader (#2262), and the shared multimodal prompt input MultimodalInput (#2306).
  • lattice-simd holds the SIMD vector math that was lattice_embed::simd: dot product, cosine similarity, Euclidean and Manhattan distance, normalization, and INT8, INT4 and binary quantized vectors with tiered prepared queries. It has no internal dependencies, so a vector index can depend on it without the embedding models. lattice-embed re-exports it as lattice_embed::simd. (#2360)
  • lattice-lora holds the LoRA adapter descriptor and its validation: the target-module allowlist, the effective-scale rule, the alpha, rank and element budget checks, and the blend planner (#2380), and since #2383 also the LoRA projection geometry, the host factor records with the scalar CPU apply and effective-weight blend, and, under its optional safetensors feature, the PEFT and MLX adapter file codec. It has no internal dependencies, and none at all without that feature. lattice-fann re-exports it at its published paths, lattice_fann::LoraDescriptor and lattice_fann::lora::*, lattice-tune keeps lattice_tune::lora::{LoraLayer, apply_lora} as re-exports, and lattice_compute::lora_hook re-exports the geometry items (#2380, #2383).
  • lattice-router holds request-level adapter routing: the on-disk gate state, bootstrap provisioning, recorded routing decisions and feedback, the lattice-fann backed mixture router, bounded refits and held-out admission evidence. lattice_inference::{router_state, router_init, router_episode, router_refit, router_admission, mixture} stay as re-exports under the same feature gates (#2334).
  • lattice-backward holds the reverse-mode gradients used for LoRA training: the operation tape, the backward operations, GQA attention backward, gradient checks and the GatedDeltaNet backward. lattice_inference::backward and lattice_inference::attention::gdn_backward stay as re-exports under train-backward (#2336).
  • lattice-forecast holds Chronos-2, Reverso and Toto 2.0 forecasting and their shared context preparation, with the same module layout and public items. lattice_inference::forecast stays as a re-export (#2332).
  • lattice-metal-runtime holds the shared Metal runtime (device, buffers, pipelines, argument encoding) and the shared Metal shader sources. lattice-inference depends on it only under metal-gpu. The crate's library is empty off macOS (#2217, #2279, ADR-103 Amendment 3).
  • Model-family crates, each enabled in lattice-inference by a default-on feature of the same name, with every 0.14 facade path kept as a re-export and a facade test that checks it names the family type:
    • lattice-model-bert: the BERT and BGE encoders, the cross-encoder reranker and their weight loaders (#2293; feature bert).
    • lattice-model-embeddinggemma2: the EmbeddingGemma 2 text and image encoders (#2284; feature embeddinggemma2).
    • lattice-model-qwen3-embedding: QwenModel, QwenEncoder and the Qwen3 Metal forward pass (#2297, renamed from lattice-model-qwen3 in #2331; feature qwen3).
    • lattice-model-gemma4: Gemma 4 CPU generation, its KV cache and preflight checks (#2298; feature gemma4).
    • lattice-model-ernie45: the ERNIE 4.5 decoder on CPU and Metal, and PaddleOCR-VL behind the crate's own paddleocr-vl feature (#2303, #2310, ADR-103 Amendment 23 in #2292; facade features ernie45 and paddleocr-vl, which implies ernie45).
    • lattice-model-qwen35: the Qwen3.5 model, its CPU, f16, Q8, NEON and Metal paths, generation sessions, speculative decoding, the MoE expert cache, the vision encoder and LoRA loading (#2312; feature qwen35).
  • lattice-serve holds the HTTP serving stack, the lattice and lattice_serve binaries and the Q4, QuaRot and MoE admission tools (#2227, #2252, ADR-103 Amendments 4 and 16). See Breaking changes.

Performance

  • Qwen3.5 attention accumulates cached values contiguously across each token's head dimensions on CPU f16, Q8, packed-Q8, NEON and speculative paths, while preserving ascending token summation and zero-weight handling (#1971).
  • CPU prefill attention packs each query tile once and reuses each packed K/V tile across the tile's query rows, scoring a whole tile through the matrix multiplication backend instead of a scalar dot product per query row and key; masking, the online softmax order and nonfinite-input behaviour are unchanged (#1981).
  • Eager Metal MoE layers route on the device: the router, top-k, weight renormalization and the shared-expert gate run as kernels inside the token's command buffer, so decode no longer waits on a host read per MoE layer. Cached Q4 expert storage keeps the host router, and LATTICE_MOE_HOST_ROUTER=1 forces it everywhere for comparison (#2214, ADR-053 amendment D3' in #2212).
  • Gemma CPU prefill evaluates long prompts in chunks of at most 64 tokens, batching the Q/K/V/O, GeGLU and per-layer embedding projections; causal and sliding-window attention per query are unchanged. Short prompts and prompts with invalid token ids stay on the sequential path (#1986).
  • CPU prefill carves its thirteen phase buffers out of one arena sized to the largest phase instead of their sum; an oversized shape is refused before any scratch is resized (#1979).
  • NativeEmbeddingService computes the BGE-small embedding-space identity once, when the model loads, instead of rebuilding and hashing the descriptor on every identified embedding call (#2365).

Correctness

  • PaddleOCR-VL reports an out-of-range text id as an error instead of panicking, and refuses mismatched placeholder and encoder-row counts before changing embeddings. EmbeddingGemma 2 also validates image-row placement before writing it (#2503).
  • Persistent Qwen embedding cache files include the SHA-256 of the loaded base-model revision in their name, so checkpoints with the same model name no longer overwrite one another's cache. When the model or tokenizer revision is unknown, only the in-memory cache is used (#2509).
  • Reading a LoRA adapter with a maximum byte limit of u64::MAX now returns the file contents instead of overflowing the read limit (#2383).
  • lattice-tune accepts adapter factors only from supported text-layer namespaces and recognized LoRA factor suffixes; factors from other towers or with unsupported suffixes now fail with the offending key. Unknown projection names under supported blocks are skipped, and the loaded adapter lists them in LoraAdapter::skipped_factor_keys. The MLX factor keys lora_a and lora_b load with or without a trailing .weight, as in 0.14.0 (#2383).
  • The CPU attention softmax behind the BERT-family encoders computes exp accurately in its scalar, NEON and AVX2 arms instead of with a bit-trick approximation. The approximation's error varied within each power-of-two interval, so it did not cancel when a row was normalised. Against the sentence-transformers reference vectors, the CPU encoder's minimum cosine rises from 0.99987 (bge-small-en-v1.5) to at least 0.999998 on every f32 model, and the parity test's floor rises from 0.9990 to 0.99999. Embeddings from 0.15.0 therefore differ slightly from those of 0.14.0 on the same input. Cost on x86_64 with AVX2 and FMA: the softmax kernel takes about 1.6x as long, a 512-token encode about 2.4% to 3.5% longer, a 128-token encode under 1%. The NEON arm is not measured (#2234).
  • With thinking disabled, a Qwen3.5 chat continuation on Metal reuses the cross-turn prefix from the previous turn's start instead of refilling the whole prompt (#2322).
  • The paged KV cache compares the stored token ids of a promoted prefix with the request before restoring it, so two different token sequences that share a hash no longer restore each other's pages (#2311).
  • Cross-turn reuse planning declines an entry retained for another slot (#2307), and the prefix page cache keeps the other entries' LRU order when it refreshes one (#2323).
  • On the Metal decode route, MoE layers routed from a hidden state the GPU had not yet written, so experts and the shared-expert gate were chosen from the previous kernel's output. Routing now reads the completed post-attention norm (#2213).
  • EmbeddingTable::write_row derives the row width from the table and panics before any write when the row id is out of range or the destination is not exactly one row wide, on both the f16 and Q4 arms. The f16 table checks its geometry against the buffer at construction (#2109).
  • The BERT and Qwen weight loaders report a rank-0 tensor as InferenceError::ShapeMismatch instead of panicking with an index out of bounds (#2299). gemma4_scaled_embedding names the out-of-range id, its position and the row count when it panics (#2309).
  • LoraAdapter::validate_against_bert in lattice-tune reads its projection shapes from the same table as validate_geometry, so the two checks cannot disagree; behaviour and messages are unchanged (#2313).
  • The Qwen3.5 Metal LoRA loader, lattice_model_qwen35::lora_file::load_lora_safetensors, which chat_metal --lora and lattice serve's /v1/lora/load use, now uses the same decoder and the same adapter_config.json rules as lattice-tune. In 0.14.0 it accepted only the base_model.model.model.layers. and model.layers. key prefixes and the self_attn and mlp blocks, so a PEFT adapter trained on the Hugging Face Qwen3.5 checkpoint, which names its tensors base_model.model.model.language_model.layers.<i>.…, failed with "no LoRA tensors found", and linear_attn factors were dropped. Both layouts now load. It also read alpha only from the safetensors header, accepted a non-finite alpha, fell back to the rank for an unparsable one, and kept one of two tensors for the same factor. Those inputs are now refused; a duplicate is refused with a message naming the two keys. A factor on a GatedDeltaNet projection the Metal path does not apply (in_proj_a, in_proj_b) makes the adapter load fail instead of being dropped. The load report names the alpha source and any skipped tensors (#2387, #2383).
  • FlatKVCache::try_new returns InferenceError::InvalidInput instead of panicking in the allocator when a per-layer buffer or the layer vector would exceed isize::MAX bytes; FlatKVCache::new is unchanged (#2375).
  • On macOS, matmul_into, sgemm_bt_strided, sgemm_nn_strided and sgemm_nn_ab no longer pass empty shapes to Accelerate, which aborted the process on a zero leading dimension: m == 0 or n == 0 writes nothing, and k == 0 zeroes the output rows (strided and matmul_into) or computes beta * C (sgemm_nn_ab) (#2357, #2377).
  • The portable GEMM fallbacks follow the same BLAS rules for empty shapes and zero alpha/beta, including overwriting a non-finite C when beta is zero (#2381).
  • The standalone WGPU GEMM (gpu_matmul, gpu_matmul_bt) returns false for recoverable failures instead of panicking: sizes are checked against the device limits before any buffer is allocated, and a failed mapping leaves the output untouched (#2352).
  • lattice-tune's safetensors adapter reader refuses a file with two tensors for the same factor (for example ….q_proj.lora_A.weight and ….q_proj.lora_A), even when their bytes are identical, and a factor pair whose A and B use different key forms (PEFT lora_A/lora_B against MLX lora_a/lora_b). Before, the decoder kept whichever duplicate it read last, in the order the file's tensor names came out of a hash map, and transposed only the MLX half of a mixed pair. Both now fail with TuneError::Serialization naming the two keys. (#2383)
  • lattice-tune::lora::load_peft_safetensors reads the PEFT adapter_config.json beside a PEFT-form adapter file and takes its lora_alpha. Before, a PEFT export, whose header carries no alpha, loaded with alpha equal to the rank; for an export with r=16 and lora_alpha=32 that applied the adapter at half its trained strength. The config's r must equal the factor rank, its target_modules (a list, or a pattern matched against the whole module path) must cover every loaded module and its exclude_modules must match none, and a header alpha must equal lora_alpha; each disagreement fails with TuneError::Validation naming both values or the module. A config using rsLoRA, DoRA, rank or alpha patterns, trained biases, modules_to_save or trainable tokens is refused rather than approximated. A missing config keeps the previous behaviour; an unreadable or malformed one is an error. MLX-form files and load_peft_safetensors_bytes, which governed admission uses, read no config. (#2383)
  • Qwen3.5 self-speculation on Metal truncates its draft to the remaining generation room before verification. With little room left, a verification could advance the KV cache past the tokens the round committed (#2495).

API

  • lattice-arch exposes the unstable argmax_f32_first_wins_usize helper used by the scalar sampler and shared by Gemma 4, Qwen3.5 speculative decoding and LoRA training. It keeps the first index on ties, ignores NaNs and returns zero for empty or all-NaN input (#2507).
  • lattice_arch::qwen_config::QwenConfig::from_json_str provides the Qwen embedding configuration parser alongside its configuration type; the model loader retains the built-in 0.6B fallback when config.json is absent (#2510).
  • LoraAdapter::skipped_factor_keys names the factor tensors a file load skipped for an unrecognised module, and LoraAdapter::alpha_rank_default reports whether the file declared no alpha, so alpha equals the rank (#2383).
  • BertModel::from_bytes_with_max_tokens loads a BERT model from memory with a caller-chosen input token cap instead of the one derived from config.json; it is marked unstable in its documentation (#2230).
  • NativeEmbeddingService::embed_identified returns vectors with an embedding-space identity (ADR-104) and, when known, the backend that produced each vector. IdentityMode and IdentifiedEmbeddings describe the consumed model, tokenizer and effective settings. A Known ID binds the consumed config, tokenizer and weights to the recipe that produced the vectors, so equal IDs identify the same space. BGE small, base and large, multilingual E5 small and base, all-MiniLM-L6-v2, paraphrase-multilingual-MiniLM-L12-v2, Qwen3-Embedding and EmbeddingGemma 2 declare recipes (#2354, #2365, #2366, #2399, #2400, #2401, #2402, #2404). BGE small supports Header mode; other models are identified only in Full mode, which hashes every consumed weight file. Header mode does not hash tensor bytes, so edits confined to tensor data do not change its identity. BERT identities use output revision bert-native-output-v2, so their IDs differ from the earlier prerelease recipe; the API was not in v0.14.0. The backend is result metadata rather than an identity input, so CPU implementations across hosts and Metal on one load share an ID (#2403); the cross-host prediction and drift floor are recorded at row L2 of decisions/LEDGER.md. Models without a config.json, including managed downloads, report Unavailable; load from a directory holding the checkpoint's config to get a Known ID.
  • EncoderIdentity::RAW_PROMPT_CONVENTION names the default "raw" prompt convention (#2288).

Serving

  • A non-streaming chat completion now returns 500 internal_error if the worker closes before sending its completion event, instead of returning partial text with zero usage as a successful response (#2413).
  • Streaming chat completions on both servers honour stream_options.include_usage. When it is true, each chunk carries usage: null and a completed stream ends with one usage-only chunk holding the engine's prompt and generated token counts, before [DONE]. A failed or interrupted stream does not send it. Without the option the stream is unchanged. stream_options on a non-streaming request is now refused with 400 unsupported_feature (earlier versions ignored the field). Unknown fields inside stream_options are ignored. (#2388)
  • Each generation provider admits a lattice serve checkpoint into a route plan that owns its selection marker, its implicit pooled-embedding action and its CPU or Metal open path; startup reads those from the selected provider (#2102).
  • Loading a LoRA adapter through lattice-serve whose factor tensors name modules the decoder does not support is now refused with 400 lora_load_failed, naming the first eight skipped tensors in sorted order and how many more there are. Earlier versions dropped those factors, printed a note to standard error and loaded the rest. lattice-model-qwen35 adds load_lora_safetensors_with_report, which returns a LoraLoadReport (effective alpha, its source, the skipped tensor names) and prints nothing; load_lora_safetensors is unchanged. lattice-inference re-exports both. (#2420)

Build and dependencies

  • lattice-embed builds its simd benchmark and native-only integration tests only when the native feature is enabled, so --no-default-features --all-targets builds no longer fail on targets that require native embedding support (#2464).
  • lattice-lora's safetensors feature adds serde_json and regex, to read PEFT adapter_config.json and evaluate its target_modules patterns. regex was already in the lockfile (#2383).
  • lattice-inference gains default-on features bert, embeddinggemma2, qwen3, gemma4, ernie45, paddleocr-vl and qwen35, one per model family (#2284, #2293, #2297, #2298, #2303, #2312). With a family's feature off, that family's modules are not compiled and the targets that reach them require the feature. Default builds include every family.
  • lattice-embed depends on lattice-model-bert, lattice-model-qwen3-embedding, lattice-model-qwen35 and lattice-model-embeddinggemma2 instead of on lattice-inference, so a normal build of it no longer compiles the facade (#2315). Its manifest documents which crate owns each forwarded feature (#2321).
  • The native Node.js binding imports BertModel from lattice-model-bert (#2320).
  • lattice-tokenizer gains a default-on parallel feature; with it off, WordPiece batches run serially and the crate has no Rayon dependency (#2216).
  • PNG and JPEG decoding moves behind a new image feature on lattice-inference and lattice-embed, on by default. With it off, the image decode entry points keep their signatures and return an error naming the feature (#2103).
  • tracing is requested once from the workspace with default-features = false, features = ["std"], and lattice-embed inherits that request instead of tracing's defaults (#2218, #2224).
  • lattice-embed builds clean under --no-default-features again: the native batch helpers are gated on native (#2225).
  • The lockfile takes tokio 1.53.3 for RUSTSEC-2026-0386, an informational unsound advisory on tokio's AsyncFd constructors; no workspace code uses AsyncFd (#2153).
  • lattice-inference no longer depends on clap: argument parsing lives with the CLI in lattice-serve (#2155, #2227).
  • Building with default features on newer stable toolchains no longer warns about unread ChatMessages fields off Metal (#2099).
  • lattice-serve declares the serde_json raw_value feature its chat request contract uses; it had compiled only because axum enabled it. Cargo.lock is unchanged (#2418).
  • A f16,metal-gpu test build of lattice-model-qwen35 no longer compiles two unread Metal ViT counter fields (#2379).

Internal reorganisation

  • The Metal Qwen3.5 engine is split into submodules by responsibility (#2115-#2123, #2125-#2133), and encoder helpers, checked buffer allocation and pipeline construction are shared across call sites (#2144, #2145, #2147). These now live in lattice-model-qwen35. Public paths are unchanged.
  • lattice_serve's structured-output code (#2136), the batched prefill kernels (#2140) and the CPU driver allocation group (#2141) move into their own files. The file-size gate now exempts no source file from its 3,000-line cap; 0.14.0 exempted nine.
  • The Qwen3 WGPU forward graph and its shaders move to lattice-model-qwen3-embedding, and the BitNet ternary kernels and the standalone WGPU GEMM to lattice-compute; lattice_inference::forward::{gpu, bitnet_kernel, gpu_gemm} re-export them (#2349, #2339). Continuous-batching scheduling moves to lattice_compute::batch, with the facade paths kept as re-exports (#2335).
  • lattice_router::serving_support::install_recorded_softmax installs a recorded softmax policy for both lattice-serve and the router's test stand-in (#2367); the CPU softmax's three vector exp arms share one set of constants, with release code generation unchanged on x86_64 and aarch64 (#2382).

Benchmarks and tooling

  • scripts/embedding_st_adapter.py creates ordered JSONL reference embeddings from a local BGE-small sentence-transformers snapshot without downloading files. It verifies the caller-supplied SHA-256 manifest and model configuration, and refuses inputs longer than the model's 512-token limit (#2459).
  • bench-compare builds the inference bench before every arm's load sampler starts, including the control arms (#2208), and gives each arm its own Cargo target directory when the caller sets one, so base and head can no longer run one shared executable (#2235, #2236).
  • On Linux, the in-phase load sampler reads per-process CPU over the same one-second window as the machine idle share, so a bench that briefly fans out over every core is no longer booked as foreign load (#2266).
  • lattice-fann's router experiment runs as an example rather than a bench target (#2226).
  • bench-compare accepts bench targets whose group names contain / (#2124), and reports an embed prebuild failure with cargo's real exit status (#2112).
  • Every crate README carries a generated reference section (facts, source layout, statistics), refreshed from main by CI (#2287).
  • The decode harness run-record schema moves into its own module (#2142); the bench supervision, GPU-handoff launch and perf-gate self-tests are split into focused files (#2138, #2139, #2150).
  • The npm release steps log the trusted-publishing token exchange at verbose level (#2101).
  • Known limitation: the lattice-embed simd bench allocates its input vectors with the default allocator, so their cache-line offset varies from run to run. On an AMD Ryzen 7 PRO 8700GE the same binary timed simd_dot_product/simd/768 between about 31 ns and 50 ns depending only on that offset. Treat run-to-run differences on the simd/* groups below that range as placement, not as a code change, until the bench pins its input alignment.
  • The machine-wide Metal test lock moves into lattice-metal-runtime; lattice_inference::measurement::gpu_test_lock re-exports it, and the lock path, wait and handoff protocol are unchanged (#2319).
  • scripts/publish.sh waits and retries when crates.io answers 429, and skips a version the registry already has for that crate, so a stopped run resumes where it stopped (#2324).
  • The bench_gpu example accepts --gpu-only, which times the WGPU forward without loading or keeping the CPU model and its comparison data (#2350).
  • The backward Stage 0 benchmark has a portable scalar arm for all eight operations, so the forced-scalar cases time real scalar code (#1977).
  • The workspace dev and test profiles build six CPU-kernel crates at opt-level = 3, which shortens cargo test on Linux; the release and bench profiles are unchanged, and profile settings do not apply to projects that depend on these crates (#2342). The wgpu-gpu feature packages are linted by clippy in CI (#2363).

Tests

  • The source-reading contracts scan every workspace package that holds serving code (#2223). The two standalone CPU generation entries have their request preflight order pinned (#2215).
  • A dependency-direction contract keeps the extracted crates from depending back on the facade (#2110), and facade parity tests pin each re-export to its definition.
  • The Gemma 4 softcap mutation witness calls the function it guards (#2308).
  • The model-format test module is declared under plain cfg(test) (#2128); the Metal measurement-lock contract is split into support modules (#2137); the source index's resolution rules are pinned in two row tables (#2151).

Documentation

  • ADR-103 records the crate split and the model layer through Amendment 23 (#2105 and later; Amendment 21 in #2263, Amendment 23 in #2292). ADR-046 records that lattice_serve constrains strict json_schema output (#2114).
  • The README describes the workspace without a fixed crate count and lists the model-family publish tier (#2302, #2305). docs/safety.md lists only the files that hold unsafe Metal code (#2193). The intra-doc links the rustdoc lint rejected are repaired (#2146, #2304), and the workspace rustdoc lint passes.
  • Every compatibility re-export in lattice-inference is marked in its documentation as removed at 1.0, naming the crate that owns the item (#2330). ADR-104 proposes embedding vector-space identity (#2353), ADR-088 is withdrawn with the attestation API (#2345), and ADR-103 Amendment 24 and ADR-102 Amendment 1 record the router, backward and compute moves and the shared Metal mechanisms (#2341, #2361).