Repository navigation
Conversation
Change map
|
|
@coderabbitai review Please review the native timestamp precision changes and their compatibility with existing second-based indexes. Focused native and SDK integration tests pass; the broader Rust suites are still completing. |
✅ Action performed
|
There was a problem hiding this comment.
Actionable comments posted: 6

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: main
- File: .coderabbit.yaml
- Commit: 3961984
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
12758629-93fe-40a9-a520-9ab9213696c5
Files selected for processing: 44-
packages/diskgraph/diskgraph-core/src/metadata.rspackages/diskgraph/diskgraph-core/src/sortkey.rspackages/diskgraph/diskgraph-core/src/time.rspackages/diskgraph/diskgraph-engine/src/engine.rspackages/diskgraph/diskgraph-index/src/bin/explore.rspackages/diskgraph/diskgraph-index/src/graph.rspackages/diskgraph/diskgraph-ingest/README.mdpackages/diskgraph/diskgraph-ingest/src/col_stats.rspackages/diskgraph/diskgraph-ingest/src/lib.rspackages/diskgraph/diskgraph-ingest/src/stage_extension.rspackages/diskgraph/diskgraph-pybind/python/diskgraph/compiler.pypackages/diskgraph/diskgraph-pybind/python/diskgraph/tt_artifacts.pypackages/diskgraph/diskgraph-pybind/python/tests/test_sampling.pypackages/diskgraph/diskgraph-pybind/python/tests/test_subsecond_time.pypackages/diskgraph/diskgraph-pybind/python/tests/test_tt_artifacts_split_marshalling.pypackages/diskgraph/diskgraph-pybind/src/custom_tt_entry.rspackages/diskgraph/diskgraph-pybind/src/tt_builder.rspackages/diskgraph/diskgraph-pybind/src/tt_executor.rspackages/diskgraph/diskgraph-rowpack/src/access.rspackages/diskgraph/diskgraph-rowpack/src/bin/rowpack_ctx_inspect.rspackages/diskgraph/diskgraph-rowpack/src/lib.rspackages/diskgraph/diskgraph-rowpack/src/parser.rspackages/diskgraph/diskgraph-rowpack/src/tests_timestamp_precision.rspackages/diskgraph/diskgraph-train-table/README.mdpackages/diskgraph/diskgraph-train-table/src/custom_tt_chunk_processor.rspackages/diskgraph/diskgraph-train-table/src/pass_b.rspackages/diskgraph/diskgraph-train-table/src/route.rspackages/diskgraph/diskgraph-train-table/src/split_decider.rspackages/diskgraph/diskgraph-train-table/src/ttgen_executor.rspackages/diskgraph/diskgraph-train-table/src/ttgen_output.rspackages/diskgraph/diskgraph-train-table/src/ttgen_resolve.rspackages/diskgraph/diskgraph-train-table/src/ttgen_stats.rspackages/diskgraph/diskgraph-train-table/src/ttgen_traversal.rspackages/kumo-core/src/kumo_core/diskgraph/ingest.pypackages/kumo-core/src/kumo_core/prediction.pypackages/kumo-core/src/kumo_core/train/diskgraph_nested_trainer.pypackages/kumo-core/src/kumo_core/train/io.pypackages/kumo-core/src/kumo_core/train/output.pypackages/kumo-core/src/kumo_core/training/table.pypackages/kumo-core/tests/test_prediction.pypackages/kumo-core/tests/test_train_output.pypackages/kumo-core/tests/test_train_table.pypackages/kumo-engine/kumo/engine/graph.pypackages/kumo-engine/test/test_prediction_output_columns.py
Files with no reviewable changes: 1-
- packages/kumo-core/src/kumo_core/train/output.py
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.
-
|
@coderabbitai review |
✅ Action performed
|
There was a problem hiding this comment.
Actionable comments posted: 1
Note
Quiet mode is enabled, so only the most important comments were posted inline. Other review comments are grouped below.

Other comments: 1
packages/diskgraph/diskgraph-rowpack/src/access.rs: 1
2504-2511:Minor · Functional Correctness · Quick win
Date32range check is lost forSecondsprecision.
get_timestamprejectsDate32days outside-106752..=24855. The reason in its comment is that seconds-based sort keys use i32 epoch seconds.
get_timestamp_with_precisionnow handlesDate32before it delegates toget_timestamp. ForSecondsit returnsdays * 86_400with only an overflow check. A date after 2038 now produces a value abovei32::MAX.encode_timestamp_valueclamps it into the last Int32 bucket, so temporal ordering is silently wrong on legacy seconds indexes.Apply the legacy range check in the
Secondsarm. Only the nanosecond arm needs the unrestricted range.Proposed fix
if matches!(array.data_type(), DataType::Date32) { let days = array.as_any().downcast_ref::<arrow_array::Date32Array>().ok_or_else(|| anyhow!("date cast"))?.value(self.row); + if precision == crate::TimestampPrecision::Seconds && !(-106752..=24855).contains(&days) { + return Err(anyhow!("date out of range: day {days}")); + } let factor = match precision {

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: main
- File: .coderabbit.yaml
- Commit: 3961984
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
554a1774-853a-4d2c-a41d-1273cfede30f
Files selected for processing: 24-
packages/diskgraph/diskgraph-core/src/sortkey.rspackages/diskgraph/diskgraph-core/src/tt_builder.rspackages/diskgraph/diskgraph-index/src/graph.rspackages/diskgraph/diskgraph-ingest/src/lib.rspackages/diskgraph/diskgraph-pybind/python/diskgraph/compiler.pypackages/diskgraph/diskgraph-pybind/python/diskgraph/tt_artifacts.pypackages/diskgraph/diskgraph-pybind/python/tests/test_compiler.pypackages/diskgraph/diskgraph-pybind/python/tests/test_subsecond_time.pypackages/diskgraph/diskgraph-pybind/python/tests/test_tt_artifacts_split_marshalling.pypackages/diskgraph/diskgraph-pybind/src/tt_builder.rspackages/diskgraph/diskgraph-rowpack/src/access.rspackages/diskgraph/diskgraph-rowpack/src/parser.rspackages/diskgraph/diskgraph-rowpack/src/tests_timestamp_precision.rspackages/diskgraph/diskgraph-train-table/README.mdpackages/diskgraph/diskgraph-train-table/src/custom_tt_chunk_processor.rspackages/diskgraph/diskgraph-train-table/src/pseudo_time.rspackages/diskgraph/diskgraph-train-table/src/split_decider.rspackages/diskgraph/diskgraph-train-table/src/ttgen_accumulator.rspackages/diskgraph/diskgraph-train-table/src/ttgen_executor.rspackages/diskgraph/diskgraph-train-table/src/ttgen_output.rspackages/diskgraph/diskgraph-train-table/src/ttgen_resolve.rspackages/diskgraph/diskgraph-train-table/src/ttgen_traversal.rspackages/diskgraph/diskgraph-train-table/tests/custom_tt_integration.rspackages/diskgraph/diskgraph-train-table/tests/ttgen_integration.rs
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.
-
|
@coderabbitai review |
✅ Action performed
|
There was a problem hiding this comment.
Actionable comments posted: 1

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: main
- File: .coderabbit.yaml
- Commit: 3961984
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
8b570156-4574-40db-9c46-1d5ca746fdb8
Files selected for processing: 3-
packages/diskgraph/diskgraph-ingest/src/lib.rspackages/diskgraph/diskgraph-rowpack/src/access.rspackages/diskgraph/diskgraph-rowpack/src/tests_timestamp_precision.rs
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.
-
|
@coderabbitai review |
✅ Action performed
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

Major · Validate timestamp precision before changing the output. · lib.rs:7114
packages/diskgraph/diskgraph-ingest/src/lib.rs:7114
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winValidate timestamp precision before changing the output.
A fresh ingest removes the local publication marker and deletes the output root before
IngestContext::newvalidates timestamp precision. A precision mismatch can therefore delete an existing index without producing a replacement. Move this validation beforeinvalidate_publication()and the fresh-output wipe. Preserve the existing Encode-marker identity when moving the validation.

Review info
-
Run configuration-
- Source mode: Auto
- Repository: NVIDIA/kumo-engine
- Branch: main
- File: .coderabbit.yaml
- Commit: 3961984
- Layers: Repository YAML
- Review profile: QUIET
- Plan: Enterprise
- Run ID:
2f3a3fd9-67b8-49d5-b7e3-30aa0bd92b05
Files selected for processing: 5-
packages/diskgraph/diskgraph-ingest/src/lib.rspackages/diskgraph/diskgraph-pybind/python/tests/test_subsecond_time.pypackages/diskgraph/diskgraph-rowpack/src/access.rspackages/diskgraph/diskgraph-rowpack/src/lib.rspackages/diskgraph/diskgraph-rowpack/src/tests_timestamp_precision.rs
Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 10 remain after this review.
-
|
@coderabbitai review The outside-diff finding about validation order is fixed in a4ee0f3. Precision normalization now runs before publication invalidation, output preparation, or a fresh-output wipe. Encode marker bytes are captured first, preserving the prior identity contract. A native regression asserts that an invalid precision configuration preserves every byte of an existing index, including its publication marker. Focused Rust checks pass; native end-to-end and resume-equivalence checks are running. Please check the complete precision-validation and publication path for any remaining issues. |
✅ Action performed
|
|
@coderabbitai review Commit aed86e2 fixes the hosted native CI findings. The observed-encoding regression now supplies one key buffer for its one-column context, satisfying the debug-build invariant; all five timestamp regressions pass with debug assertions enabled. The timestamp reader uses |
✅ Action performed
|
|
@coderabbitai review |
✅ Action performed
|
RowAccess carries the index clock and keeps main's parsing and null rules; only the final conversion now targets the clock instead of seconds. Typed timestamps and integer epochs round up, text fractions truncate, decimal epochs truncate to the finer of their unit and the tick and then round up, and Date32 multiplies. Seconds conversions are identities, so seconds rowpacks are unchanged. Decoders emit Arrow timestamps in the clock's unit (seconds for hours and days). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The ingest config gains a top-level timestamp_unit that defaults to seconds when absent. The saved diskgraph_config.json and index_metadata.json always record it, and every row access reads it. An Encode marker written before the clock was recorded still resumes, read as seconds. Edge time keys stay 32-bit for seconds, keep their configured width for hours and days, and use the full 64-bit value for sub-second clocks; out-of-range edge times still clamp to the far-future key. TT-NG timestamp grouping stays one second wide, and stage extensions inherit the parent's clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Decoders, temporal validity checks and the sampler take the clock from index metadata. Validity columns keep main's conversions (an Int64 column is epoch nanoseconds, a Date32 whole days), seed timestamps still truncate into the index clock, and decoded seed TIMESTAMP columns are read in their own Arrow unit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Plan execution dispatches once on the index clock. The builder keeps plan times in nanoseconds and writes the plan in the clock it was created with, converting with main's truncation; DiskGraphPlanCompiler(index_path=...) uses the index's clock, and a non-seconds plan carries a timestamp_unit tag that must match the index. Datetime literals still round up and custom-table timestamps still truncate, now to the index clock. Outputs stay timestamp[ns], saturating, and random splits keep hashing whole seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Timestamp column statistics hold index-clock ticks. col_stats.json keeps min_secs/max_secs for seconds and otherwise names the bounds after the clock (min_ms, min_hours, ...) and adds timestamp_unit. resolve_temporal and compute_col_stats_for_parquet take an optional timestamp_unit, default seconds. Date statistics and batch-prediction histograms are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On whole-day data every clock decodes, samples, generates the training table and computes statistics exactly like the seconds index. On a messy fixture each source type follows main's null and rounding rule in every clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Vedaant and I talked this through offline. Here's where we landed, followed by the changes I'm requesting. Seconds stays the default, and finer precision is opt-in. Warehouse timestamps usually carry sub-second digits whether or not those digits mean anything, for example when an ETL job stamps rows with The comments below cover three things: closing the same-second leak in the default clock, which this PR doesn't do; making the opt-in deliver what the description promises; and the shape of the API. Correctness of the default seconds clockBoth of these are bugs on
Correctness when someone opts in
API and ergonomics
Description and claims
Other things to consider
The cost of opting in, and an idea for reducing itA snapshot stores each timestamp more than once on purpose. Each edge's sort key carries the neighbor's timestamp, so the sampler can find the part of an adjacency list that's visible at an anchor by binary search, without reading every neighbor's row. The row store keeps its own copy for features and training-table generation. The total is one row copy plus one copy per time-keyed edge direction. Every foreign key on an event table adds a reverse direction keyed by the event's time, and forward directions into tables with their own time column add more. An event table with three foreign keys stores each timestamp at least four times. When a user opts in, the row copy is a varint and grows by only 1–3 bytes, but every edge-key copy doubles from 4 to 8 bytes. Edge keys therefore dominate the opt-in cost, and the PR only benchmarks the seconds clock. Before we settle the design, it would be worth measuring index size and sampling throughput for seconds vs. milliseconds on a representative dataset. If the 64-bit key cost turns out to be large, one option is to keep 32-bit seconds keys even in an opted-in index and recover exactness elsewhere:
That keeps edge keys at today's size. The cost is extra row reads for neighbors in the boundary second. That's cheap for typical data but expensive for the dense clickstream data most likely to opt in, so it's only worth doing if the measured key cost is large. I'm not suggesting it for this PR. |
The row reader picks its generic timestamp_in::<clock> once, and the ingest conversions are generic over the clock with one arm per source unit, so every factor is a compile-time constant. On the seconds clock this compiles to main's hard-coded divisions and the text fraction is never parsed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A source value beyond the range of the index clock (for example year 9999 on a nanosecond index) now reads as an error, so ingest stores a null cell and drops the row when the column is a required time column, as main does for Date32 values outside its range. Data values are no longer saturated. Every real value fits the seconds clock, so seconds indexes are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Calendar arithmetic returns None instead of panicking, and a row whose window, lag or split shift runs past the clock is dropped rather than wrapped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Kumo timestamp strings and training-table dates print the fraction a finer clock stores, and nothing extra when it is zero, so seconds output is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
DiskGraphTTBuilder held plan times in nanoseconds, so a seconds or millisecond plan clamped anchors outside 1677-2262. It now keeps each anchor, split bound and offset as given and converts it straight into the plan clock in build(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Written into the ingest config only when it is not seconds, so seconds configs stay identical to main. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seconds keeps main's snapshot cache key; every other clock gets its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Training and prediction pass index_path to DiskGraphPlanCompiler, the runtime adapter and NodeRange anchors understand hours and days indexes, and timestamp statistics keep every nanosecond digit of their fraction. A temporal-LP training batch takes its valid RHS items at its earliest anchor, since its anchors share a second rather than a tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clock is sent as a query parameter only when it is not seconds, and the SDK requires the server to echo the clock it built; a missing echo means an older server, which builds seconds. Without a clock, snapshot() keeps the clock of the graph's current snapshot, so the implicit calls in fit and predict reuse it instead of rebuilding in seconds. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The clock travels from the snapshot request through GraphDataComputeInput, GraphDataVersionInfo and DiskgraphIngestArgs into the ingest config. Seconds keeps main's snapshot identity; other clocks hash their own. The refresh workflow passes the clock to the ingest child only behind the diskgraph-index-clock-v1 patch. The router echoes the clock recorded for the built snapshot in X-Kumo-Timestamp-Precision. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Batch-prediction statistics read the clock from the snapshot's index_metadata.json when the snapshot was not built in seconds, falling back to seconds when the file is missing or unreadable. Python sample statistics convert through resolve_temporal in that clock; typed seconds columns keep their vectorized path, which returns the same values. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Six-clock acceptance in kumo-engine and kumoai_studio, the unchanged seconds cache key and hosted identity, the clock echo for old and new SDKs and servers, the patched ingest workflow argument, statistics in the snapshot's clock, and NodeRange and seed times on hours and days indexes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t version Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Typed columns take the same vectorized conversion in every clock, which matches resolve_temporal; text statistics truncate into the snapshot clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
83552d8 to
f42fb95
Compare
This PR adds index clocks: a DiskGraph index can store its timestamps in nanoseconds, microseconds, milliseconds, seconds, hours or days. Seconds stays the default, and a seconds index stores exactly the same data as one built on
main. The clock changes only the tick that timestamps are stored in and the integer width used to store them; nulls, rounding, truncation, saturation, errors, statistics and prediction output followmain's rules in every clock.Problem
Teams with event data finer or coarser than one second ask:
12:00:00.500. Why can it miss an event from12:00:00.300, or see one from12:00:00.700?Cause
Every index stores time as epoch seconds. Ingest converts each source timestamp to whole seconds (typed timestamps and integer epochs round up, text truncates), so everything finer than a second is gone before sampling or training-table generation sees it.
DatasetMetadata.timestamp_unitexists, but ingest always writes"seconds"and the sampler asserts it.Change
Choose the clock when building a snapshot. The default is seconds.
The index records its clock, and everything that reads it (sampling, training-table generation, statistics, prediction) works in that clock.
From a millisecond snapshot:
What changes with the clock
mainmainmainRow timestamps are variable-length integers, so their size follows the clock (a 2026 timestamp takes 3 bytes in days, 5 in seconds, 9 in nanoseconds). On a 2M-user × 60M-event fixture a milliseconds index is 11.8% larger than a seconds one (2,856 vs 2,554 MB), 240 MB of it from the 64-bit edge time keys and 62 MB from row timestamps; ingest, sampling and training-table times are within noise of seconds.
What stays as on
main, in every clockmain's accepted rangemaindatetimeis local time, apd.Timestampis UTCtimestamp[ns]/timestamp[us]exactly asmainTwo rules are needed only because a clock can now be too narrow for a value, which never happens in seconds:
maindoes for out-of-range Date32 values.Kumo's default prediction anchor comes from the latest timestamp statistic. Statistics now print the stored fraction, and the anchor parser adds the digits beyond microseconds that Python's
datetimedrops, so on a nanosecond index the default anchor equals the latest event.Back-compat
index_metadata.jsonand statistics byte-identical tomain.diskgraph_config.jsongains one line,"timestamp_unit": "seconds", so its hash iningest_stats.jsonchanges.timestamp_unitmain.DiskGraphPlanCompiler(index_path=...)writes plans in the index clock; every Kumo caller passes it).diskgraph-index-clock-v1patch.timestamp[us].Known limitations
main.timestamp[us], so a nanosecond index's exported anchors are truncated to the microsecond.Testing Done
main. An index built from a fixture with every source type (fractional text with and without formats, s/ms/µs/ns epochs, decimals, Date32 beyond 2038, typed timestamps, nulls, time-keyed edges) is byte-identical tomain's apart from the addedtimestamp_unitline indiskgraph_config.json(and its hash) and files that also differ between two runs ofmain. Sampled subgraphs and every training-table file match.main's null and rounding rule per source type on messy data.main, medians of 9 interleaved runs on 5M rows: text +0.2%, decimal −3.3%, Arrow −1.7%, integer epochs +3.3% wall time at equal CPU time. Training-table generation on 2M users × 60M events could not be separated from noise on a shared machine: across three interleaved runs, day windows were 4–13% faster thanmainand month windows 0–12% slower.timestamp[us]; the seconds snapshot id equalsmain's.cargo clippy --all-targets -D warningsclean for diskgraph-core, -rowpack, -ingest and -pybind;cargo test --releasefor core, rowpack and ingest 1212 passed, plus one disk-pressure test that measures free space on a busy shared filesystem and passes on a quiet one;test_index_clocks.py17 passed; kumoai_studio 654 passed; kumo-core runtime and pquery with kumo-api 1285 passed; kumo-engine snapshot and graph tests 30 passed; hosted snapshot, data-backend, refresh-workflow, connector, datamodule and link-prediction module tests 403 passed. The remaining kumoai_studio failures come from an incompatible local pyarrow and an auto-init check.🤖 Generated with Claude Code