Skip to content

fix(generation): keep timeseries prompt context in the training serialization - #684

Merged
binaryaaron merged 2 commits into
mainfrom
agonzales/timeseries-prefill-serialization
Aug 5, 2026
Merged

fix(generation): keep timeseries prompt context in the training serialization#684
binaryaaron merged 2 commits into
mainfrom
agonzales/timeseries-prefill-serialization

Conversation

@binaryaaron

@binaryaaron binaryaaron commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

The timeseries sliding-window prompt context diverged from the serialization the model is trained on, in three ways (all reproduced at runtime against main):

  1. Serialization dialect. TimeseriesBackend._update_group_state rebuilt each group's prefill with json.dumps(record, ensure_ascii=False) — spaces after : and ,, unescaped /, full-repr floats. Training examples serialize via pandas to_json(orient="records", lines=True) — no spaces, \/ escaping, double_precision=10. From the second batch of every group onward, the prompt continued in a JSON dialect the model never saw in training:

    training: {"ts":"2024-01-01 00:00:00","url":"a\/b","fval":0.123456789,"ival":1}
    rebuilt:  {"ts": "2024-01-01 00:00:00", "url": "a/b", "fval": 0.123456789, "ival": 1}
    
  2. Blank lines in the initial prefill. SequentialExampleAssembler._get_initial_prefill joined already newline-terminated record lines with "\n", producing \n\n between records — training examples have single newlines.

  3. Shape drift between batches. The initial prefill (leading space, \n\n separators) and the rebuilt prefill (no leading space, \n separators) did not match each other, so batch-1 and batch-2+ prompts differed beyond record content.

Fix

  • Rebuild the prefill from each ParsedRecord.text — the exact bytes the model emitted, which are already in the training dialect. Re-serializing the parsed dicts through records_to_jsonl was rejected: pandas serializes column-wise, so a null sharing the 3-record window upcasts another record's 1 to 1.0, and double_precision=10 re-rounds accepted model output.
  • GroupState.recent_records now carries ParsedRecord (was dict) so the emitted bytes survive the sliding window; _retain_single_valid_response returns the retained ParsedRecords.
  • Both prefill producers now emit the same shape: a leading space followed by newline-terminated records (" {r1}\n{r2}\n{r3}\n"). The leading space is kept because the initial-prefill shape is persisted in trained-model metadata; the rebuilt prefill is pure runtime state.

Besides output quality, byte-alignment of prompt context with the trained output format is a prerequisite for prompt-lookup speculative decoding (drafts only match if the prompt text token-matches what the model emits) and for stable per-group prefix caching.

Tests

  • tests/data_processing/test_assembler.py::test_sequential_assembler_initial_prefill now pins the exact prefill bytes (previously it filtered out the blank lines rather than pinning them).
  • tests/generation/test_timeseries_backend.py::TestUpdateGroupState now pins the exact rebuilt-prefill bytes and the verbatim reuse of emitted text.
  • mise run format / typecheck clean; affected suites (52 tests) pass.

Compatibility

The prefill string is consumed only by _format_prompt and _get_timestamp_from_prefill (regex-based, shape-agnostic) — both dialects parse identically, so no downstream consumer changes. Already-trained model artifacts keep their persisted initial prefill; the D2 fix affects newly trained models only.

Summary by CodeRabbit

  • Bug Fixes

    • Improved prefill reconstruction to preserve exact record formatting, including spaces and line breaks.
    • Fixed sliding-window processing to retain original emitted text and handle empty timestamp column names correctly.
    • Prevented extra blank lines when combining sequential sample text.
  • Tests

    • Expanded coverage for formatting preservation and timestamp handling.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change preserves emitted record formatting when rebuilding timeseries prefills. It also updates sliding-window state to store ParsedRecord objects and validates exact formatting and empty timestamp-column handling.

Changes

Prefill record handling

Layer / File(s) Summary
Assembler prefill concatenation
src/nemo_safe_synthesizer/data_processing/assembler.py, tests/data_processing/test_assembler.py
Initial prefills concatenate newline-terminated samples directly. Tests verify exact JSONL formatting, ordering, spacing, and termination.
Parsed record retention
src/nemo_safe_synthesizer/generation/timeseries_backend.py
Sliding-window retention and group result processing now use ParsedRecord objects.
State prefill reconstruction
src/nemo_safe_synthesizer/generation/timeseries_backend.py, tests/generation/test_timeseries_backend.py
Group state rebuilds prefills from original emitted text and reads timestamps from parsed payloads. Tests cover exact formatting and empty timestamp-column names.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested labels: bug

Suggested reviewers: mckornfield

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: preserving training serialization in timeseries prompt context.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agonzales/timeseries-prefill-serialization

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.76923% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
..._safe_synthesizer/generation/timeseries_backend.py 61.53% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

last_record = records[-1]
timestamp_seconds = self._parse_timestamp_seconds(last_record.get(self._time_column))
last_parsed = records[-1].parsed or {}
timestamp_value = last_parsed.get(self._time_column) if self._time_column else None

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Preserve empty-string timestamp column names

timestamp_column is typed as str | None, and the config/preflight code treats only None as absent, so "" remains a valid column name when the input DataFrame has such a column. The previous last_record.get(self._time_column) updated this case, but this truthiness check skips it and leaves last_timestamp_seconds stale. On the next batch, chronological validation compares against the old timestamp and rejects otherwise contiguous records. Please check self._time_column is not None (or access the key directly) instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

addressed! as an aside, should we disallow "" as a col name? that seems confusing in cases

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, we probably should!

@seayang-nv

Copy link
Copy Markdown
Contributor

Thank you for catching this!

…lization

The timeseries sliding-window prompt context diverged from the
serialization the model is trained on, in three ways:

- Dialect: _update_group_state rebuilt the per-group prefill with
  json.dumps (spaces after ':' and ',', unescaped '/', full-repr
  floats), while training examples serialize via pandas to_json
  (no spaces, escaped slashes, double_precision=10). From the second
  batch of every group onward, the prompt continued in a dialect the
  model never saw in training.
- Blank lines: _get_initial_prefill joined already newline-terminated
  record lines with "\n", inserting blank lines between records; a
  shape absent from training examples.
- Shape drift: the initial prefill (leading space, "\n\n" separators)
  and the rebuilt prefill (no leading space, "\n" separators) did not
  match each other, so batch-1 and batch-2+ prompts differed beyond
  record content.

Fix: rebuild the prefill from each ParsedRecord's original emitted
text (which is already in the training dialect) instead of
re-serializing parsed dicts; re-serializing via records_to_jsonl
would upcast ints to floats when a null shares the 3-record window
and re-round float precision. GroupState.recent_records now carries
ParsedRecord so the emitted bytes survive the sliding window, and
both prefill producers emit the same shape: a leading space followed
by newline-terminated records.

Tests pin the exact prefill bytes on both producers; the previous
assertions filtered out the blank lines rather than pinning them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
@binaryaaron
binaryaaron force-pushed the agonzales/timeseries-prefill-serialization branch from 0319c47 to 4de4816 Compare August 3, 2026 18:21
Signed-off-by: Aaron Gonzales <aagonzales@nvidia.com>
@binaryaaron
binaryaaron marked this pull request as ready for review August 3, 2026 18:28
@binaryaaron
binaryaaron requested a review from a team as a code owner August 3, 2026 18:29
@binaryaaron
binaryaaron requested a review from seayang-nv August 3, 2026 18:29
@coderabbitai coderabbitai Bot added the bug Defects in shipped behavior label Aug 3, 2026
@binaryaaron
binaryaaron requested a review from zywind August 3, 2026 18:29
@greptile-apps

greptile-apps Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Aligns time-series prompt context with training serialization.

  • Concatenates already newline-terminated training records without introducing blank lines.
  • Preserves retained ParsedRecord.text verbatim when rebuilding sliding-window prefills.
  • Keeps initial and rebuilt prefills consistent with a leading space and newline-terminated records.
  • Adds exact-byte and timestamp-column regression coverage.

Confidence Score: 5/5

The PR appears safe to merge, with the changed training and generation serialization paths remaining aligned.

The assembler guarantees newline-terminated record text, generated ParsedRecord text is framed correctly when rebuilding the window, and all consumers of the changed retained-record type explicitly receive the expected ParsedRecord or parsed-dictionary representation.

Important Files Changed

Filename Overview
src/nemo_safe_synthesizer/data_processing/assembler.py Directly concatenates the assembler's newline-terminated records, removing unintended blank lines while preserving the persisted prefill shape.
src/nemo_safe_synthesizer/generation/timeseries_backend.py Retains ParsedRecord objects and rebuilds each sliding-window prefill from emitted text without changing validated-record ordering or downstream dictionary contracts.
tests/data_processing/test_assembler.py Pins the exact initial-prefill bytes for multiple groups.
tests/generation/test_timeseries_backend.py Verifies exact rebuilt-prefill serialization and timestamp extraction for an empty-string column name.

Sequence Diagram

sequenceDiagram
  participant Assembler
  participant Metadata
  participant Backend as TimeseriesBackend
  participant Model
  Assembler->>Metadata: Persist initial prefill from newline-terminated JSONL
  Metadata->>Backend: Seed GroupState.current_prefill
  Backend->>Model: Generate continuation
  Model-->>Backend: ParsedRecord objects with emitted text
  Backend->>Backend: Retain best valid response
  Backend->>Backend: Rebuild prefill from exact ParsedRecord.text
  Backend->>Model: Generate next sliding-window batch
Loading

Reviews (1): Last reviewed commit: "fix(generation): preserve empty timestam..." | Re-trigger Greptile

@seayang-nv seayang-nv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Thanks!

@binaryaaron
binaryaaron added this pull request to the merge queue Aug 5, 2026
Merged via the queue into main with commit 920d17c Aug 5, 2026
25 of 26 checks passed
@binaryaaron
binaryaaron deleted the agonzales/timeseries-prefill-serialization branch August 5, 2026 14:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants