Skip to content

Commit 0a59cb2

Browse files
yatesdrlukealonso
andauthored
Add Messages cache checkpoint hints (#28)
* Pass Messages cache controls through captured template strings Carry user block ends into native and Jinja checkpoint marks. Preserve byte positions through captured content macros, concatenation and trim without changing prompt text. Validation: 55 frontend and 10 native protocol CPU tests passed, including the real Qwen template, repeated text and UTF-8 trimming. The hardware run reproduced lost Qwen hints; the corrected hardware rerun is pending. No graph or package format changes. Design-Signed-Off-By: yatesdr; design-review=6aeb * Document model renderer and checkpoint requirements Require model integrations to preserve canonical prompt semantics, emit exact complete-message endpoints, carry advisory block hints, and validate Jinja and native encoders against independent references. Link the contract from agent guidance and describe single-pass rendering in the core design. Validation: checked documentation against frontend implementations, verified 103 local documentation links, and passed git diff --cached --check. Documentation only; runtime and GPU tests were not run. Design-Signed-Off-By: Codex (GPT-6); design-review=6aeb * Preserve cache hints for images and segmented templates Carry image checkpoint hints by source index and resolve them after image expansion to complete prepared image spans. Forward hints to segmented Jinja rendering, preserve string positions through JSON escaping, and map them into decoded segment bytes before token boundary validation. Prompt text, token IDs, special-token selection, and image conditioning remain unchanged. The implementation uses shared frontend paths. Validation: 934 Rust tests passed, 34 ignored; all-target check with server,runtime-package,test-tools passed. Qwen checkpoint fixtures passed 82 cases, and both Kimi checkpoint parity tests passed. The release CLI version check and Python 3.12 extension import passed. Repeated Qwen/Kimi renderer benchmarks retained unhinted allocation counts and bytes across all ten render workloads; latency measurements varied between runs. GPU serving and live cache reuse were not rerun. Design-Signed-Off-By: Codex (GPT-6); design-review=6aeb --------- Co-authored-by: Luke Alonso <lalonso@gmail.com>
1 parent d367edc commit 0a59cb2

32 files changed

Lines changed: 1410 additions & 201 deletions

‎AGENTS.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ and hardware sections of the preparation guide.
9595

9696
For sampling or frontend metadata changes, also read
9797
[sampling and frontend metadata](docs/model-development.md#sampling-and-frontend-metadata).
98+
Model onboarding and changes to tokenizers, chat templates, or native prompt
99+
encoders must satisfy the [renderer contract and validation requirements](docs/model-development.md#prompt-renderers-and-checkpoint-boundaries).
98100

99101
## Build and run model checkpoints
100102

‎docs/core-design.md‎

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1078,13 +1078,17 @@ retain the first failure, and a failed CUDA drain retains the graph and buffer o
10781078
### Native frontend dependencies
10791079

10801080
Helios uses Axum/Tokio for HTTP, Hugging Face `tokenizers` for tokenization,
1081-
and MiniJinja with Python-method compatibility for chat templates. Dependency
1082-
versions and features are specified in [Cargo.toml](../helios/Cargo.toml).
1083-
The [template renderer](../helios/src/frontend/mod.rs) supplies Python-compatible
1084-
JSON formatting, whitespace handling, and template exceptions. Rendering must
1085-
match the recipe's canonical template bytes and token IDs, including tool schemas
1086-
and image content. Recipes normally use the checkpoint template; Gemma 4 uses
1087-
Google's canonical templates across quantization publishers as specified in
1081+
and its compiled Jinja VM or a native encoder for prompt rendering. MiniJinja is
1082+
a development-only differential and benchmark reference. Dependency versions and
1083+
features are specified in [Cargo.toml](../helios/Cargo.toml).
1084+
The [native frontend](../helios/src/frontend/mod.rs) preserves the authoritative
1085+
renderer's prompt bytes, token IDs, tool serialization, and image order. Jinja
1086+
evaluation supplies Python-compatible JSON formatting, whitespace handling, and
1087+
template exceptions. Model integrations must satisfy the
1088+
[renderer contract and validation requirements](model-development.md#prompt-renderers-and-checkpoint-boundaries).
1089+
1090+
Recipes normally use the checkpoint template. Gemma 4 uses Google's canonical
1091+
templates across quantization publishers as specified in
10881092
[frontend metadata](model-development.md#sampling-and-frontend-metadata).
10891093

10901094
The [native image processor](../helios/src/frontend/images/mod.rs) is checked
@@ -1348,16 +1352,21 @@ Only consumed history can define a checkpoint.
13481352

13491353
### Exact endpoint capture
13501354

1351-
The shared frontend renders each message prefix without a generation header or
1352-
assistant continuation. It accepts every nonempty rendered token sequence that
1353-
is a strict prefix of the complete prompt, irrespective of message roles. A
1354-
prefix may contain a template-generated preamble or end after any system,
1355-
developer, user, assistant, or tool message. Templates can reject an incomplete
1356-
conversation after emitting a complete prefix; only emitted tokens matching the
1357-
validated full prompt are eligible. Context-dependent rendering that changes
1358-
previous tokens cannot create a checkpoint for that prefix. Equal endpoints are
1359-
deduplicated. Image placeholder expansion shifts endpoints to their positions in
1360-
the prepared token stream; no endpoint may split an atomic image span.
1355+
The shared frontend renders the complete normalized conversation once. Annotated
1356+
Jinja templates record zero-output byte marks, segmented templates record counts
1357+
of completed segments, and native encoders record normalized-message byte ends
1358+
as they append output. Completed preambles and messages of every supported role
1359+
are eligible; the open assistant generation header is excluded.
1360+
1361+
Shared mapping converts those positions through the full prompt encoding's
1362+
untrimmed token offsets. An exact cut requires every preceding token to end at
1363+
or before it and every following token to start at or after it. BPE tokens
1364+
crossing a mark and overlapping Unicode spans cannot create partial-token cuts.
1365+
Continuation truncation excludes later positions. Token endpoints are sorted,
1366+
deduplicated, and strictly inside the prompt. Image placeholder expansion shifts
1367+
them into the prepared token stream without splitting atomic image spans. Cache
1368+
reuse depends on the actual prepared prefix identity; context-dependent changes
1369+
to earlier tokens cannot reuse a checkpoint for different tokens.
13611370

13621371
Endpoints are not rounded to token groups or KV pages. The scheduler obtains a
13631372
snapshot destination when it reaches an endpoint, using normal cache eviction.

‎docs/model-development.md‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,148 @@ templates do not select rendering behavior. Helios normalizes JSON-string tool
6161
arguments into objects before rendering. Tests compare the assets with the pinned
6262
Google sources and exercise prompt bytes, reasoning, tool rounds, and cache boundaries.
6363

64+
## Prompt renderers and checkpoint boundaries
65+
66+
Every model integration must provide prompt rendering, tokenization, image
67+
ordering, and reusable-prefix endpoints through the shared native frontend.
68+
These requirements apply to text Jinja, segmented Jinja, and programmatic Rust
69+
encoders. A plausible generated answer does not establish frontend correctness.
70+
Follow the [frontend metadata contract](../helios/model-contract.md#frontend-implementations)
71+
and [native frontend interfaces](../helios/README.md#native-frontend).
72+
73+
### Canonical prompt semantics
74+
75+
- Identify the exact checkpoint tokenizer and authoritative template or encoder
76+
revision. Preserve prompt bytes, token IDs, special-token treatment, whitespace,
77+
tool serialization, reasoning history, generation headers, and continuation
78+
behavior. A deliberate inference transformation needs an explicit semantic
79+
requirement and independent reference tests.
80+
- Render the normalized conversation supplied by the shared request path.
81+
Protocol decoding, instruction lowering, and tool-result normalization belong
82+
in shared code. Renderer selection describes prompt syntax; it must not select
83+
scheduling, cache policy, or model execution behavior.
84+
- Return image sources in the same order as their rendered placeholders. If
85+
normalization reorders tool results, move each result's images with its text.
86+
Preserve the tokenizer's distinction between ordinary text and special tokens,
87+
including literal special-token spellings in user and tool content.
88+
- Explicit request options override packaged defaults. Validate unsupported
89+
options and malformed content explicitly. Preserve previously exported
90+
packages' renderer selection, absent-field defaults, and packaged semantics.
91+
92+
### Complete-message endpoints
93+
94+
- Render the conversation once and record endpoints while producing its output.
95+
Tokenize each required text or ordinary/special-token segment once. Do not
96+
recover boundaries by rendering or tokenizing every conversation prefix,
97+
searching for message text, or inserting marker text into the prompt.
98+
- Record the completed preamble and every completed normalized message in output
99+
order, including system, assistant, and tool messages where supported. A merged
100+
user run has the endpoint of the merged message; reordered results have
101+
endpoints in rendered order. Include closing wrappers and separators belonging
102+
to a completed message, and exclude the open assistant generation header.
103+
- Text renderers report UTF-8 byte positions, not character counts or token IDs;
104+
segmented Jinja reports segment counts as specified below. Shared boundary
105+
mapping uses the full encoding's untrimmed offsets and accepts
106+
an exact cut only when every preceding token ends at or before it and every
107+
following token starts at or after it. Omit an exact cut crossed by a BPE token
108+
or overlapping Unicode spans; do not round it to a nearby token or cache page.
109+
- The frontend's final token endpoints must be sorted, unique, and strictly
110+
inside the prompt. Continuation truncation must exclude later endpoints. Image
111+
expansion must shift endpoints with the token stream without splitting an
112+
atomic image span. The scheduler owns prompt/terminal checkpoints and complete
113+
state capture; renderers supply positions, not cache-management instructions.
114+
115+
### Jinja integration
116+
117+
- Package an annotated recipe template. Removing its mark annotations must
118+
reproduce the authoritative template bytes, including any explicitly supported
119+
inference transformation. Preserve Jinja whitespace-control behavior and test
120+
actual rendered output; source equality alone does not validate placement.
121+
- Text templates use `{% mark %}` to record a zero-output byte position.
122+
Templates producing token segments use `{% mark expression %}`, where the
123+
expression is the number of completed segments. For example,
124+
`{% mark ns.segments | length %}` refers to segment entries, not bytes in the
125+
serialized JSON representation. Preserve each segment's `allow_special` value.
126+
- Place marks at root output sites after a completed preamble or message.
127+
Marks inside macros, captured assignments, or filter blocks are forbidden;
128+
mark the root call site after the helper returns. Handle each output branch,
129+
including tool-only messages and empty content with nonempty framing.
130+
- Verify the recipe exports the annotated asset and Helios loads it through the
131+
shared compiler. Do not bypass the missing-annotation error or add a broad
132+
template fallback. Compatibility substitutions must match the exact template
133+
bytes of an existing supported package.
134+
135+
### Programmatic encoder integration
136+
137+
- Use the authoritative encoder where available. Expose normalized-message byte
138+
endpoints from its actual append loop, and adapt them through the shared
139+
tokenizer/boundary path. Avoid a parallel implementation of the encoder's
140+
formatting or normalization solely to discover positions.
141+
- Carry positions through system insertion, adjacent-message merging, tool-result
142+
sorting, and wrapper emission. Keep image sources, message text, and endpoint
143+
ownership together through those operations. Both Jinja and native encoders
144+
must satisfy the same complete-message and token-boundary invariants.
145+
- Maintain required upstream changes as focused commits in a pinned dependency
146+
fork. Keep related crates' revisions and shared types coherent, preserve the
147+
upstream license, and document dependency updates. Compare against the
148+
independent upstream renderer when changing prompt behavior; calling the same
149+
patched encoder through another adapter is not an independent reference.
150+
151+
### Content-block checkpoint hints
152+
153+
For requests carrying advisory block hints, every supported rendering mode must
154+
satisfy these requirements:
155+
156+
- Decode protocol-specific fields into neutral message/block positions before
157+
rendering. Hints must not alter prompt bytes, token IDs, image order, reasoning
158+
state, or explicit request options, and must not reach model-visible output.
159+
Requests from protocols that do not supply hints retain their ordinary behavior.
160+
- Preserve block identity through normalization and rendering. Carry positions
161+
through byte-preserving concatenation, captures, macros, segment construction,
162+
and trimming where their output positions remain defined. Repeated equal text
163+
must not cause a hint to move to a different block. Never guess a position after
164+
an unmappable transformation.
165+
- Cover text blocks, supported image blocks, and whole tool results. A tool-result
166+
endpoint includes its closing wrapper; an image endpoint must survive atomic
167+
image expansion. Text-only coverage does not establish image or segmented
168+
renderer support. Report unsupported cases explicitly rather than silently
169+
treating an accepted hint as implemented.
170+
- Keep advisory cuts distinct from exact message marks. A hint may use the last
171+
safe complete-token prefix before its byte endpoint; it must not weaken exact
172+
message-boundary validation or change scheduler/cache policy.
173+
- Preserve an unhinted path without hint-specific string copies or per-value
174+
hint allocations. Measure unhinted latency and allocations when changing shared
175+
rendering machinery; protocol-specific decoding alone does not isolate its cost.
176+
177+
### Required validation
178+
179+
1. Compare the production renderer with the pinned authoritative reference using
180+
the actual template/encoder and checkpoint tokenizer. Check rendered bytes,
181+
token IDs, image order, supported options, and expected errors. Exercise
182+
system/developer lowering, user and assistant turns, reasoning on/off where
183+
supported, tool choices, multiple/reordered results, and continuation.
184+
2. Test boundary behavior with empty and repeated content, UTF-8, whitespace/BPE
185+
joins, merged users, wrappers, generation headers, and image expansion.
186+
For block hints, include a stable history plus a changing suffix inside one
187+
user message; assert the expected extra cut and identical hinted/unhinted
188+
prompts. Exercise captures and macros with the real recipe template, and
189+
segmented and image paths where supported. Synthetic tokenizers are useful
190+
edge-case tests but do not establish checkpoint-tokenizer parity.
191+
3. Exercise the native request preparation used by each supported HTTP protocol
192+
and load the exported package's frontend assets/settings. Validate older
193+
packages when changing their loading or default semantics.
194+
4. Demonstrate repeated-prompt and changed-suffix cache reuse through the actual
195+
service, including ordinary and speculative execution where supported. Check
196+
consumed-prefix accounting and continuation correctness. Frontend CPU tests
197+
alone do not establish runtime cache reuse or a serving performance gain.
198+
Report missing checkpoints, unrun hardware paths, and skipped tests explicitly.
199+
200+
Use [frontend fixtures](../tests/frontend),
201+
[native boundary tests](../helios/tests/message_boundaries.rs),
202+
[native encoder/request tests](../helios/tests/deepseek_requests.rs), and
203+
[HTTP protocol tests](../tests/server/test_protocols.py) as starting points.
204+
Keep ordinary tests and measurements; do not add development audit artifacts.
205+
64206
## Weight formats and arithmetic precision
65207

66208
- **Read quantization from checkpoint metadata.** Use `config.json` declarations

‎helios/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ blake2 = "0.10"
3838
libloading = "0.8.9"
3939
libc = "0.2.189"
4040
tokenizers = { version = "=0.23.2", default-features = false, features = ["onig"], optional = true }
41-
serde_json = { version = "1.0", features = ["preserve_order", "float_roundtrip"], optional = true }
41+
serde_json = { version = "1.0", features = ["preserve_order", "float_roundtrip", "raw_value"], optional = true }
4242
sha2 = { version = "0.10.9", optional = true }
4343
ryu = { version = "1.0", optional = true }
4444
png = { version = "0.18.1", optional = true }

‎helios/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,13 @@ token starts at or after it. Overlapping Unicode spans and BPE tokens crossing a
432432
create partial-token cuts. Token endpoints are sorted, unique, and strictly
433433
inside the prompt. Image expansion adjusts them with one ordered pass.
434434

435+
Advisory content-block hints travel separately from prompt text. Text hints keep
436+
their positions through captured strings and JSON serialization; segmented
437+
templates map JSON string positions back to decoded segment bytes before token
438+
mapping. Image hints retain indices into the rendered image-source list and
439+
resolve to the ends of prepared image spans after expansion. Hints do not change
440+
prompt bytes, token IDs, image conditioning, or ordinary/special-token selection.
441+
435442
`Frontend::encode`, `decode`, and `incremental_decoder` use Hugging Face's Rust
436443
tokenizers library pinned to the fixture environment's version. Incremental
437444
decoding preserves incomplete UTF-8 between chunks and flushes remaining bytes

‎helios/benches/template.rs‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ fn main() {
221221
let expected = template.render(&context).unwrap();
222222
let iterations =
223223
(50_000_000 / start.elapsed().as_nanos().max(1)).clamp(1, 5000) as usize;
224-
let actual = compiled.render(context.as_object().unwrap()).unwrap();
224+
let actual = compiled.render(context.as_object().unwrap(), &[]).unwrap();
225225
assert_eq!(actual.text, expected, "{name}/{case}");
226226
if check_only {
227227
println!(
@@ -242,7 +242,7 @@ fn main() {
242242
measure(&format!("owned,{name},{case}"), || {
243243
black_box(
244244
compiled
245-
.render(black_box(context.as_object().unwrap()))
245+
.render(black_box(context.as_object().unwrap()), &[])
246246
.unwrap(),
247247
);
248248
});
@@ -252,7 +252,7 @@ fn main() {
252252
black_box(output);
253253
});
254254
concurrent(&format!("owned,{name},{case}"), iterations, || {
255-
black_box(compiled.render(context.as_object().unwrap()).unwrap());
255+
black_box(compiled.render(context.as_object().unwrap(), &[]).unwrap());
256256
});
257257
}
258258
}

‎helios/src/frontend/boundaries.rs‎

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub(super) fn preserve_offsets(tokenizer: &mut Tokenizer) {
2020

2121
pub(super) fn token_ends(
2222
bytes: &[usize],
23+
hints: &[usize],
2324
offsets: &[(usize, usize)],
2425
text_len: usize,
2526
) -> Result<Vec<usize>> {
@@ -38,19 +39,32 @@ pub(super) fn token_ends(
3839
for i in (0..offsets.len()).rev() {
3940
suffix_start[i] = suffix_start[i + 1].min(offsets[i].0);
4041
}
41-
let mut result = Vec::with_capacity(bytes.len());
42+
let mut cuts = Vec::with_capacity(bytes.len() + hints.len());
43+
for &byte in bytes {
44+
cuts.push((byte, false));
45+
}
46+
for &byte in hints {
47+
if byte <= text_len {
48+
cuts.push((byte, true));
49+
}
50+
}
51+
if !hints.is_empty() {
52+
cuts.sort_unstable();
53+
}
54+
let mut result = Vec::with_capacity(cuts.len());
4255
let mut tokens = 0;
4356
let mut prefix_end = 0;
44-
for &byte in bytes {
57+
for (byte, hint) in cuts {
4558
let byte = byte.min(text_len);
4659
while tokens < offsets.len() && prefix_end.max(offsets[tokens].1) <= byte {
4760
prefix_end = prefix_end.max(offsets[tokens].1);
4861
tokens += 1;
4962
}
50-
// Overlapping offsets can represent several tokens for one Unicode scalar.
63+
// Hints may floor a crossing token; overlapping Unicode spans still cannot split.
64+
let boundary = if hint { prefix_end } else { byte };
5165
if tokens > 0
5266
&& tokens < offsets.len()
53-
&& suffix_start[tokens] >= byte
67+
&& suffix_start[tokens] >= boundary
5468
&& result.last() != Some(&tokens)
5569
{
5670
result.push(tokens);
@@ -66,18 +80,41 @@ mod tests {
6680
#[test]
6781
fn cuts_require_all_tokens_on_their_respective_side() {
6882
assert_eq!(
69-
token_ends(&[0, 1, 2, 3, 4, 6], &[(0, 2), (2, 4), (2, 4), (4, 6)], 6).unwrap(),
83+
token_ends(
84+
&[0, 1, 2, 3, 4, 6],
85+
&[],
86+
&[(0, 2), (2, 4), (2, 4), (4, 6)],
87+
6
88+
)
89+
.unwrap(),
7090
vec![1, 3]
7191
);
7292
assert_eq!(
73-
token_ends(&[2, 3, 4, 5], &[(0, 2), (4, 5), (2, 4), (5, 6)], 6).unwrap(),
93+
token_ends(&[2, 3, 4, 5], &[], &[(0, 2), (4, 5), (2, 4), (5, 6)], 6).unwrap(),
7494
vec![1, 3]
7595
);
7696
assert_eq!(
77-
token_ends(&[2, 3, 4, 100], &[(0, 2), (4, 6)], 6).unwrap(),
97+
token_ends(&[2, 3, 4, 100], &[], &[(0, 2), (4, 6)], 6).unwrap(),
7898
vec![1]
7999
);
80-
assert!(token_ends(&[4, 2], &[(0, 2), (2, 6)], 6).is_err());
81-
assert!(token_ends(&[2], &[(0, 7)], 6).is_err());
100+
assert!(token_ends(&[4, 2], &[], &[(0, 2), (2, 6)], 6).is_err());
101+
assert!(token_ends(&[2], &[], &[(0, 7)], 6).is_err());
102+
}
103+
104+
#[test]
105+
fn only_hint_cuts_floor_crossing_tokens() {
106+
assert_eq!(
107+
token_ends(&[3], &[3], &[(0, 2), (2, 5), (5, 6)], 6).unwrap(),
108+
[1]
109+
);
110+
assert!(
111+
token_ends(&[3], &[], &[(0, 2), (2, 5), (5, 6)], 6)
112+
.unwrap()
113+
.is_empty()
114+
);
115+
assert_eq!(
116+
token_ends(&[], &[3, 7], &[(0, 2), (2, 4), (2, 4), (4, 6)], 6).unwrap(),
117+
[1]
118+
);
82119
}
83120
}

0 commit comments

Comments
 (0)