Repository navigation
v0.11.0
lattice v0.11.0
Minor release, and under Cargo's rules for a 0.x line that means a breaking one: ^0.10 does not admit 0.11, so the minor position is where an incompatible change goes. Thirteen breaking changes are listed at the end of these notes, grouped by crate: three in lattice-embed, one in lattice-transport, eight in lattice-inference and one in lattice-tune. We count one change for each public type or function whose callers have to change, and we group items that have to change together, such as both fields of one struct. The list combines a cargo-semver-checks comparison against 0.10.0 with a comparison of public function signatures, which covers changed parameter and return types that the semver tool does not check.
The headline is embeddings and serving. lattice-embed gains EmbeddingGemma 2 and now reports per-input token counts and truncation. Both HTTP servers can keep several LoRA adapters resident and let each chat request pick a weighted mix of them. A long list of correctness fixes covers grammar-constrained decoding, the multi-token-prediction draft head, prompts longer than 4,096 tokens, and a CPU tanh approximation that was off by up to 9.1e-3.
New: EmbeddingGemma 2
lattice-inference adds the EmbeddingGemma 2 text encoder, and lattice-embed registers it as EmbeddingModel::EmbeddingGemma2.
- The CPU path runs in f32. It reads only the text tower of the checkpoint, uses bidirectional attention with a two-sided 512-position window on the sliding layers, mean-pools over every token and L2-normalizes the result.
- Output widths of 768, 512, 256 and 128 are supported.
- Every input is wrapped as
<bos> text <eos>. The tokenizer limit defaults to 8192 tokens and can be changed or removed withwith_max_tokens. lattice-embedlooks for the checkpoint inLATTICE_EMBEDDINGGEMMA2_MODEL_DIR, and otherwise in~/.lattice/models/embeddinggemma-2.- Against a reference implementation on the released checkpoint, 24 inputs of 10 to 8,473 tokens produced identical token ids, and the cosine similarity to the reference was at least 1 - 7.2e-12 at all four widths (#1841).
- An opt-in Metal path,
MetalEmbeddingGemma2State, runs all 24 layers, the final norm and the projection in one f32 command buffer.EmbeddingGemma2Model::token_states_metalandencode_ids_at_widths_metaluse it.lattice-embedkeeps using the CPU path. Against the CPU path the worst cosine was 1 - 3.9e-12 (#1844).
New: token counts and truncation reports in lattice-embed
EmbeddingService truncates each input at the model's token limit without telling the caller. A byte budget does not bound the token count, so a caller could not tell when only a prefix of a text was embedded. This release adds two methods that report it.
count_tokens(texts, model, role)tokenizes without embedding and returns oneTokenCountper input.embed_with_report(texts, model, role)returns the vectors together with oneTokenCount { before_truncation, embedded }per input.TokenCount::truncated()is true when the input was cut.- Both counts are taken on the sequence the model sees, so they include special tokens and any role instruction.
- Both methods have default bodies that return the new
EmbedError::Unsupported, so existing implementors ofEmbeddingServicekeep compiling. NativeEmbeddingServiceimplements both methods for every text model it serves, which are the BERT family, Qwen3 Embedding and EmbeddingGemma 2. Vectors fromembed_with_reportare bit-identical to those fromembed_with_role.CachedEmbeddingServiceserves vectors from its cache and takes counts from the wrapped service.EmbeddingModel::max_input_tokens()now reports 2048 for both Qwen3 Embedding variants, the limit a loaded model applies, where it reported 8192 and longer input was cut at 2048 without a signal (#1851). Size chunks fromcount_tokenswhen the exact count matters.lattice-inferenceaddsEmbeddingTokenCounts,BertModel::embedding_token_counts,QwenModel::embedding_token_countsandEmbeddingGemma2Model::tokenize_with_countsas unstable API.
New: resident LoRA adapters and per-request mixtures
lattice serve and lattice_serve can now keep several LoRA adapters resident and apply an ordered mixture per chat request. These routes need the Metal backend. On the CPU backend lattice serve answers 400 with lora_unsupported_backend.
POST /v1/lora/loadreturns a stable id and does not replace an adapter that is already applied.GET /v1/loralists the resident adapters and the applied selection.POST /v1/lora/unloadremoves one id.- A chat request picks adapters with the
lorafield, for example"lora": [{"id": 0, "scale": 0.5}, {"id": 1, "scale": 1.0}]. Unknown and repeated ids produce a named 400 error before the request is admitted, and the check runs again after the request leaves the queue. - The
lorafield has three states. If it is absent, the server may route the request. If it is[], the caller asked for the base model and a router must not override that. If it is non-empty, the request is served as named. - Loads are deduplicated by name and path, so repeating a load returns the existing id.
--max-resident-adapters(default 32) and--max-resident-adapter-bytes(default 512 MiB) bound the resident set. A load that would exceed either limit is refused withlora_residency_limit_exceeded, and nothing is evicted. GET /v1/lorareportsroutableonly when a routed blend can run. It now checks the summed rank, the blend size and matching projection shapes, where before it looked only at adapter names.lattice serve --router-stateloads a routing gate artifact at startup and routes chat requests through it. It requires a separate--embedding-modeldirectory. The gate is produced offline, and on this path every routed adapter is applied at equal weight. The macOS release build now enables themixturefeature, so a releasedlatticeno longer accepts--router-stateonly to refuse it.- With the
mixturefeature,AdapterRoutertakes a weight policy. Equal weight (1/k) stays the default, and a learned softmax policy is added with a floor on its temperature (default 0.01). Under any policy,set_epsilonsets a floor (off by default) that drops adapters whose weight magnitude falls below it. A refit is rejected once the mean entropy of its weights stays below a floor (default 0.1) for three consecutive rounds; one round at or above the floor resets the count. - In
lattice-fann,RlooTrainergains an entry point for learned mixture weights, and its load-balance term now follows observed traffic. The old term pulled every single decision toward uniform, which penalised a gate that was routing one request confidently and correctly.
New: option-letter scoring for Qwen3.5
Qwen35Model::score_option_letters (CPU) and MetalQwen35State::score_option_letters (Metal) prefill the prompt once from a fresh state, read the last-position logits at the given option-letter token ids, and return OptionScores { logits, probs, label_mass }. label_mass is the letters' share of the full next-token distribution. Both scorers return an error rather than a score for an empty prompt, no letters, a letter id outside the vocabulary, duplicate letter ids, a prompt longer than capacity and non-finite logits.
New: ERNIE-4.5 on Metal, and stricter Gemma 4 config checks
forward::metal_ernie45adds an explicit Metal backend for ERNIE-4.5 text prefill, checked against the CPU reference on every logit, and then cached decode with device-resident K/V rows and one[T,H,W]RoPE position per token.- The Gemma 4 loader implements the E2B text decoder only.
Gemma4Config::from_config_json_strnow refuses a non-nulluse_bidirectional_attention, a config for a different model role, and configs that switch on features the loader has no code for, such as a mixture-of-experts block. Each error names the field, and the check runs before any weight file is opened.
Serving
- Prompts longer than 4,096 tokens are no longer cut silently. The Metal worker of
lattice_servetokenized chat prompts with the default 4096-token cap, so a prompt between 4096 tokens and the model's window passed the window check and then reached generation cut to its first 4096 tokens. The worker tokenizer's cap is now raised to the model's context window, and the over-window refusal inlattice servereports the untruncated length. - The same fix applies to the CPU backend of
lattice serve, where a 4097 to 8192 token prompt was generated from its first 4096 tokens, and tolattice chat. On the Metal backendlattice chatkeeps a 4096-token cache, so a longer prompt is now refused there instead of cut.Qwen35Model::ensure_tokenizer_max_seq_lenis added as unstable API for this and never lowers a cap. - The pooled
/v1/embeddingsroute of thelatticebinary is bounded. At most four jobs run at once, and a fifth request gets 503server_busy. A request whose items total more than 4 x 8192 tokens gets 400batch_token_budget_exceeded. POST /v1/embeddingsrefuses a text item shaped like a data URI with 400data_uri_as_text_iteminstead of embedding the base64 text.- The grammar cache of
lattice_serveis keyed on the vocabulary as well as the schema. - Metal command buffers created by the Qwen3.5 decode paths, the Metal matmul and the Qwen3-Embedding forward are now released inside autorelease pools. Without a pool they live until the thread's outermost pool drains, and a serving thread never drains it. In four paired runs of 3,000 single-token decode steps on Apple silicon, the unpooled arms grew by 1,262 and 1,535 bytes per step, and the pooled arms changed by -16 and 339 bytes per step (#1623).
Grammar-constrained decoding
- Grammar matching keeps a set of parse stacks instead of one. When two alternatives of a rule began with the same byte, the matcher committed to the first one that accepted the byte, so input that only a later alternative could finish was rejected. This affected GBNF alternatives, optional object keys that share a prefix, and
anyOfover objects and numbers in JSON Schema. - An accepting state now continues while a longer alternative is still live, so
"a" | "aa"can generate"aa". Matching is bounded by limits on live stacks, expansion steps and parse depth, and reaching a limit is reported asStackLimitExceededand never as a rejection. A cyclic grammar now fails engine construction with the limit error instead of building a grammar that accepts nothing. - A nullable alternative listed before a non-empty sibling is retried when the next byte fails. With
r ::= "" | "a"androot ::= r "b", the inputabwas rejected and is now accepted. - GBNF group nesting is limited to 128 levels. A grammar of 100,000 nested groups used to overflow the stack and abort the process.
CompiledGrammar::root()no longer panics on a grammar without rules. GrammarState::for_grammarstarts a state as complete when the root rule accepts the empty string.GrammarState::initial()takes no grammar and always starts incomplete.- Special added tokens and base-vocabulary tokens with characters outside the byte-level alphabet now contribute no bytes to BPE decoding and to the grammar byte table.
- Building the vocabulary partition for a grammar now walks the vocabulary's byte trie once per grammar state. For a JSON schema with ten optional properties and the Qwen tokenizer (248,320 tokens), partition build time was 55.17 s and 79.85 s at the base and 4.50 s and 5.72 s with the change, on a shared x86_64 Linux host. The PR's last change, an explicit-stack walk, measured 1.03 to 1.10 times the earlier walk's partition build time (#1807).
- Keeping several stacks live has a cost on schemas with many optional keys that share prefixes. On a schema with ten such keys, engine construction took about 1.7 times as long and the per-request mask total about 2.1 times, mean of two runs per arm. On the two other schemas measured, construction changed by 0.92 and 1.12 times and the mask total by 1.33 and 1.01 times (#1818). The partition speedup in the previous item was measured separately and is not the net change between the two releases for that schema class.
Speculative decoding with the MTP draft head
- Qwen3.5 checkpoints store every RMSNorm weight for the shifted convention
x / rms(x) * (1 + gamma). The draft head used the plain convention: the CPU head at all seven of its norm sites, and the Metal path at its two pre-fusion norms, on decode and on prompt rows. Every draft-head norm now uses the shifted convention, as the rest of the Qwen3.5 forward already did. - The draft head is fed the target's hidden state after the final norm, which is the vector the language-model head reads and what the reference implementations feed it. On QuaRot-rotated checkpoints the final-norm scale is folded into
lm_headat conversion, so the head keeps the pre-final-norm state there.MetalQwen35State::mtp_hidden_tap()reports which one is in use, andLATTICE_MTP_VERBOSElines carrytap=. The change makes no claim about acceptance rate. - Accepted draft tokens now get their rows in the draft head's K/V cache. The Metal greedy loop appends the missing row for a fully accepted draft token. The generic loop
mtp_verify_draft_with_seedwrites one row per accepted token from the target hidden state that predicted it, using two new provided methods onMtpTargetVerifier,last_hiddenandverify_tokens_with_hidden, which returnNoneby default so existing implementors behave as before. The hidden-state parameter of the public draft functions is now namedhidden_predicting_current_token. MtpWeights::load_from_sourcenow loads the dense MTP layer of the Qwen3.5-0.8B checkpoint, in addition to the routed mixture-of-experts layout. It detects the layout per layer and refuses a layer with both or neither.- With a LoRA adapter loaded and
LATTICE_MTP_BATCH=1, verification ran against the base model while drafting and decoding used the adapter. Verification now keeps the adapter path.
Sampling and numerics
- Temperature scaling no longer overflows. Three sampling routes multiplied finite logits by
1 / temperaturebefore centering, so at a temperature below 1 a large logit became infinite and ties broke toward the lowest token id. Those routes now subtract the largest finite logit first, and so do the logprob and speculative softmax sites. min_pis now a field ofSamplingConfigandGenerateConfig, with a default of 0.0 (disabled), and the CPU and Metal decode paths read it. Before, onlySampler::with_min_pcould set it.- The CPU
fast_tanhused bygelu,gelu_scalarand the fusedadd_bias_geluhad an absolute error of up to 9.1e-3, which reached 2.5e-2 through the tanh-form GELU. It now uses a different rational approximation with a measured maximum error of 3.48e-7 for tanh and 7.16e-7 for GELU on x86_64. The kernel is slower: thegelugroups measured about +31% on Apple silicon, which the PR measured as +1% to +4% of a full bge-small encode for inputs of 30 tokens or more (#1845). - The GDN decay gate no longer loses the softplus tail. Below about -16.6 in f32 it read exactly 1, meaning no decay, where the true value is below 1.
- The Metal GDN Q/K L2 normalisation now includes the same epsilon as the CPU path and the Hugging Face reference.
- Caller-supplied adapter weights are floored on magnitude, not on signed value, which matters for negative scales.
Loading, memory and robustness
- A default build of
lattice-inferenceno longer fails at load time on F16 and BF16 safetensors tensors. Thef16feature still gates F8 materialization. - Loading a BF16 checkpoint no longer holds two f32 copies of the weights at the load peak. On Linux x86_64, loading Qwen3.5-0.8B and generating 12 tokens peaked at 5.77 GiB of anonymous memory before and 2.99 GiB after, with identical output, in one run per arm (#1800). Gemma 4 per-layer embeddings now stay BF16 in the mapped file and are widened one row per token.
- Checkpoint loaders refuse a path that is not a regular file. A FIFO with no writer used to block the loading thread forever on Unix, and the non-Unix open paths did no file-type check at all.
- Loading a LoRA adapter onto a QuaRot-rotated base now resolves a missing seed to the base model's own seed. Before, the adapter rotation was skipped silently. An object-form Q4 manifest whose rotation seed is missing is refused when no other source supplies one.
- A nested
text_config.rope_parameters.rope_thetais read when the flat field is absent. Before, such a checkpoint loaded without error and built RoPE tables at the wrong base unless its theta was 1e7. PaddleOcrVlModel::rope_indexreturns an error for an empty grid, and invalid ERNIE axes are rejected, where both panicked.BertModel::encode_batchno longer allocates a[num_heads, seq_len, seq_len]score buffer per packed sequence. It works one head at a time.FifoSchedulercaps admissions at the free GDN state slots. MoE expert-slot caches are sized against the memory that remains for the layers not yet sized.lattice-embedmoveslruto 0.18 to clear RUSTSEC-2026-0253. The PR found the advisory unreachable from the crate.
Streaming decode and the Metal prefix cache
- A rejected final flush is reported as
StopReason::Interrupt, and the prefix cache no longer saves a boundary that covers text the caller never received. - The prefix-cache slot stays empty when the final flush completes a stop string after a stop token.
- A new prompt that ends exactly on a retained cross-turn checkpoint no longer plans an empty replay, which Metal
forward_prefill_fromrefuses on every retry. - A library caller that sets a grammar, thinking and a reasoning budget together no longer hits a panic when the grammar rejects the forced think-close token. No shipped binary reached that combination.
- The Metal session's decode cursor is derived from the KV cache length, so it cannot fall out of step with it.
Environment switches are read by value
Switches such as LATTICE_OFFLINE, LATTICE_GDN_CPU, LATTICE_NO_GPU, LATTICE_SELF_SPEC, LATTICE_MTP and LATTICE_MTP_BATCH were enabled whenever the variable was present, so NAME=0 turned them on. Twelve switches in one change and the two MTP switches in another now treat an unset variable, an empty value, 0, false, no and off (any case) as off, and everything else as on. NAME=1 behaves as before. crates/inference/CONFIG.md now lists every LATTICE_* variable that library and binary code reads, and a check keeps the table complete.
Other crates
lattice-transport:DriftReportreportsconverged, taken from the solver on both the balanced and unbalanced paths. The unbalanced path honoursSinkhornConfig::error_on_non_convergence. Drift analysis readssource_modelandtarget_modelfrom all records instead of the first, ignores records with no model, and refuses a side that names more than one model with the newSinkhornError::MixedModelInput.lattice-fann:DiagonalFisher::project_deltaappliesdelta_i / (1 + alpha * F_i / F_ref)and no longer scales the coordinate with the largest Fisher value to exactly zero.lattice-tune: with thetrain-backwardfeature, the LoRA trainer's forward pass batches each projection across the training rows into one matrix multiply. On Qwen3.5-0.8B with sequence 512, 4 training rows and rank 8 on a shared x86_64 Linux host, the baseline scoring pass took 80.2 s and 74.3 s before and 36.2 s and 40.1 s after, step time went from 31.87 and 32.75 s to 21.51 and 21.16 s, and the training loss matched to four decimals (#1801).
Performance
These numbers come from the measurements in the cited pull requests. Hosts, sample counts and caveats are in those pull requests.
- On x86_64 Linux,
matmul_btsplits large products across rayon's global pool. In a paired comparison on a 16-thread host, Qwen3.5-0.8B CPU prompt prefill at 512 tokens had a median time ratio of 0.408 (14.08 s to 5.77 s), and the single-thread control read 1.026 (#1817). - On non-macOS x86_64, rows of a small-
mproduct beyond the last full 4-row tile go to the direct SIMD kernel. A per-matmul probe on a one-thread AVX-512 Linux host, in ABBA order, measured 6.6x to 8.8x less time warm and 4.1x to 6.0x less cold formfrom 1 to 3. End-to-end decode speed was not measured (#1805). - With a LoRA adapter loaded, dense Qwen3.5 prefill on Metal now runs batched. On an Apple M4 with the 0.8B Q4 checkpoint and a rank-8 adapter on every servable module, time to first token at 512 prompt tokens went from 4738.5 ms to 480.1 ms (#1816).
- Sampling with
min_pandtop_k = 0rejects the tail before sorting. On the full-vocabulary arm of the sampler benchmark the change read -22.75%, with an ABBA bound of [-36.98%, -5.30%], and the order-bias bound was about as large as the effect, so the size of the gain is not pinned down (#1750). - The grammar partition build (see above, #1807), the LoRA trainer forward (see above, #1801) and the BF16 load peak (see above, #1800) have their own measurements in the sections where they appear.
Internals worth knowing
- Generation now runs through one shared decoder driver. The CPU, Metal, Gemma 4, grammar and logprobs entry points were moved onto it one at a time, with golden tests recorded before the move.
GenerateConfig,GenerateOutput,TokenLogprobandTopLogprobare now defined at the crate root oflattice-inference. Themodel::andmodel::qwen35_configpaths remain as type aliases.- The Metal serving worker is built through a runtime factory, chat prompts go through per-family prompt adapters, and omitted chat options stay distinguishable until defaults apply.
Tests, CI, documentation and tooling
This release also carries 98 changes to tests, continuous integration, documentation, design records, dependency updates, release checks and benchmark tooling. They are not listed one by one here; the pull request history has each of them.
Breaking changes
Thirteen changes, grouped by crate. The pull request number follows each heading.
lattice-embed
1. EmbeddingModel variants after Qwen3Embedding4B have new discriminants (#1841). EmbeddingGemma2 was inserted between Qwen3Embedding4B and AllMiniLmL6V2, and the enum has no explicit discriminants. AllMiniLmL6V2 moved from 7 to 8, ParaphraseMultilingualMiniLmL12V2 from 8 to 9, and TextEmbedding3Small from 9 to 10. Code that casts an EmbeddingModel to an integer, or stores such an integer, must derive it again. Use the model name through Display, FromStr or the serde name. Serde writes variant names, so JSON is unaffected, while formats that write a variant's position see the shift.
2. The attestation size constants are renamed (#1710). MIN_ATTESTATION_REPORT_BYTES and MAX_ATTESTATION_REPORT_BYTES are now MIN_SUPPLEMENTARY_ATTESTATION_EVIDENCE_BYTES and MAX_SUPPLEMENTARY_ATTESTATION_EVIDENCE_BYTES. The values are unchanged at 1 and 4096. Rename the imports.
3. OpaqueAttestationReport is replaced by SupplementaryAttestationEvidence (#1710). Construct it with SupplementaryAttestationEvidence::try_new(algorithm, digest, payload) instead of OpaqueAttestationReport::try_from_bytes(bytes). AttestationAlgorithm is new and has one variant, Sha256V1. The digest is a [u8; 32] recorded as claimed, and only the payload length (1 to 4096 bytes) is validated. as_bytes() still borrows the payload. CheckpointAttestor::finish now returns Result<SupplementaryAttestationEvidence>, so implementors change the return type.
lattice-transport
4. DriftReport gained a field (#1703). DriftReport has a new public field converged: bool, which is false when the Sinkhorn solve stopped at max_iterations without converging. The struct has only public fields and is not #[non_exhaustive], so an exhaustive struct literal needs the new field. DriftReport derives Deserialize and the new field has no serde default, so JSON written by 0.10 needs converged added before it deserializes.
lattice-inference
5. GrammarState and StackFrame lost fields (#1818). GrammarState.stack: Vec<StackFrame> is replaced by stacks: Vec<Vec<StackFrame>>, the live parse stacks, which are sorted and free of duplicates after every accepted byte. StackFrame.consumed is removed, and StackFrame::alt_idx can now be UNCHOSEN_ALT for a rule that was entered without choosing an alternative. Build states with GrammarState::initial() or GrammarState::for_grammar(&grammar) (added in #1825) and read stacks where you read stack.
6. StepResult and SimResult have a new variant (#1818). Both gained StackLimitExceeded. A match without a wildcard arm needs a new arm. The variant means the matcher hit its stack limits, not that the grammar rejected the byte, so test for StepResult::Accepted instead of Rejected when you ask whether a byte went through.
7. Grammar matching functions return a Result (#1818). GrammarEngine::advance returns Result<bool, GrammarError> instead of bool, and ByteTrie::mask returns Result<(), StackLimitError> instead of (). The error means the matcher hit its stack limits; on that error mask leaves the logits untouched. Handle the error where you used the plain value.
8. CompiledGrammar::root returns an Option (#1823). It returns Option<&Rule> instead of &Rule, and None for a grammar without rules, where it used to panic.
9. AdapterCommand variants changed shape (#1633, #1644). Load { layers, descriptor, quarot_seed } is now Load { name, path, layers, descriptor }, where name and path are String and quarot_seed is gone. The unit variant Unload is now Unload { id: u32 }. Callers of MetalWorkerClient::submit_adapter_command construct the new shapes, and its reply channel now carries Result<AdapterControlResult, AdapterControlError> instead of Result<(), String>.
10. moe_expert_cache_num_slots takes two more parameters (#1715). The function in forward::moe_expert_cache now takes already_allocated_bytes: u64 and moe_layers_already_sized: usize after recommended_max_working_set_size. Passing 0 for both reproduces the earlier calculation.
11. MetalWorker::spawn takes a third parameter, and spawn_with_vision changed (#1644, #1789). The new spawn parameter is residency_limits: ResidencyLimits, with the fields max_adapters and max_bytes. Both must be positive, or startup fails with StartupError::InvalidResidencyLimits. ResidencyLimits::default() gives 32 adapters and 512 MiB. spawn_with_vision no longer takes a loader closure and a VisionRuntime: it takes an internal factory type, the pending-job bound and ResidencyLimits, and returns a fourth tuple element. The factory type is not public API, so a caller outside the crate uses spawn, which has no vision path.
12. Five serving request types are #[non_exhaustive] (#1634). ChatRequest, Message, ImageUrl, ResponseFormat and JsonSchemaFormat in serve::contract can no longer be built with a struct expression outside lattice-inference. Deserialize the request JSON instead, and write .. when you destructure one. Their public fields stay readable and assignable, and JSON field names, defaults and validation are unchanged. ChatRequest also gains an optional lora field in this release (#1633), and the attribute means a later new field will not break callers.
lattice-tune
13. The online router refit moved out of lattice-tune (#1692). lora::router_update and its items (update_router, FeedbackEvent, PreferenceSignal, ReplayBuffer, RouterDelta, RouterUpdateConfig) are no longer defined in lattice-tune, and a default build of lattice-tune no longer has the lora::router_update module. The code now lives at lattice_fann::training::router_update, behind the online-router feature of lattice-fann. With the mixture feature of lattice-tune, which turns that feature on, the old path still resolves as a re-export of the new module. Its functions report FannError (which gains a Validation variant) where they reported TuneError. Callers enable mixture or import from lattice-fann, and handle FannError.
Upgrading
Most of this release is additive. These changes need source updates:
- Code that casts
EmbeddingModelto an integer, uses the attestation names, or implementsCheckpointAttestorneeds the changes in items 1 to 3. - Code that constructs
DriftReportwith a struct literal needsconverged(item 4). - Code that reads or builds
GrammarState, matches onStepResultorSimResult, callsGrammarEngine::advance,ByteTrie::maskorCompiledGrammar::root, buildsAdapterCommandvalues, callsmoe_expert_cache_num_slots,MetalWorker::spawnorspawn_with_vision, or builds the five serving request types with struct expressions needs items 5 to 12. - Code that imports
lattice_tune::lora::router_updateenables themixturefeature or imports fromlattice-fann, and handlesFannError(item 13).
These changes need no source edits and are worth knowing:
- An environment switch set to
0,false,no,offor an empty value is now off. It was on before. - Grammar matching accepts input that only a later alternative could finish, and
"a" | "aa"can generate"aa". A cyclic grammar fails engine construction. - The CPU tanh approximation behind
geluandadd_bias_geluis accurate and slower, as described under Sampling and numerics. DiagonalFisher::project_deltano longer zeroes the update for the coordinate with the largest Fisher value.- Prompts between 4,097 tokens and the model's window are generated whole instead of cut to 4,096 tokens. Where the window itself is 4,096 tokens, as in
lattice chaton Metal, a longer prompt is refused. EmbeddingModel::max_input_tokens()reports 2048 for the Qwen3 Embedding variants instead of 8192. Code that chunks by this value produces smaller chunks that fit the limit the model applies; a role instruction still counts against that limit.