Skip to content

Name the storage choices in-memory, persistent and none - #148

Merged
zaoxing merged 5 commits into
mainfrom
feat/storage-choice-names
Sep 24, 2026
Merged

zaoxing merged 5 commits into
mainfrom
feat/storage-choice-names

Conversation

@zaoxing

@zaoxing zaoxing commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

Stacked on #146 (which is on #143). Merge those first; GitHub then retargets this PR.

The three choices

MonitoringConfig.storage_backend is now the user's storage choice, one of dmi.config.USER_STORAGE_CHOICES:

Value Meaning
"in-memory" Records delivered in memory. Until its consumer interface exists, this runs the C++ ClickHouseRecordSink and needs a host engine.
"persistent" The native capture storage path: object store + catalog + ClickHouse.
"none" Capture off entirely. No ring is allocated (no pinned staging or payload memory). A record runtime, a host engine, an explicit ring_config and adapter attachment are refused, each with a message naming the two choices that capture.
  • "auto" stays the unset default. It's the inference every caller relied on before the field existed (for example MonitoringConfig(schedule=...), and vLLM, which passes config=None). It isn't a user choice; the configurator will always emit one of the three.
  • The old names still work, with a DeprecationWarning: "native" means "in-memory" and "capture" means "persistent". MonitoringConfig keeps what the caller wrote, so an integration reading the field back (the Megatron branches use "native") still sees its own value. The engine acts on the new canonical_storage_backend.

Behaviour change: none

"none" used to mean "capture and transport, no persistence". The ring still ran, and every record then failed at flush_and_wait with "record sink is not configured". Now it's a clear refusal up front and the ring memory is never allocated. The one test that expected an adapter to attach under "none" now expects the refusal.

Evidence

  • New tests/test_storage_backend_choices.py (13 cpu tests): the three choices; auto as the default; the aliases and their warning; the unknown-value message listing the three choices; and none allocating no ring and refusing a ring_config, a record runtime, a host engine and attachment. It failed at import before the change.
  • CPU tier: 2238 passed, 1 skipped (no CUDA device), with 0 deprecation warnings across the whole tier, so nothing in src/ still uses an old name.
  • GPU (RTX 4090): the capture storage, sink ring and record-ring refusal suites, 29 passed, run with -W error::DeprecationWarning.
  • Docs: the v1 contract lists the three choices, the aliases and what none refuses; the capture-storage design doc uses the new names. benchmarks.md is a dated log and keeps the names it was written with.

Not in this PR: the configurator YAML output block (plan milestone C2), which will emit these three values.

Since the independent review (2026-09-24)

An independent review found two majors and several minors; all are fixed, test-first (58f9462..90ef0e8):

  • none refuses a ring enabled after construction too. The public enable_ring_transport() used to allocate a ring and make it active under "none"; it now raises the same "turns capture off" error as create_record_runtime.
  • The old names are pinned at the engine. Reverting the engine's mapping used to leave all 85 related tests green. The mapping is now a module-level canonical_storage_backend() applied to any config, including duck-typed ones, and engine-level tests drive each old name. With the mapping reverted, 3 of them fail.
  • The canonical name has its own Literal type (CanonicalStorageBackend).
  • The deprecation warning names the caller's line even through dataclasses.replace().
  • Refusals quote the name the caller wrote: 'native' (now 'in-memory').
  • none is tested at all five HF entry points, not only through the helper.
  • The docs' comparison table no longer calls the persistent path "Python reference-only", and the v1 contract lists each refusal with its exception type.

One review note left as a design choice: a config written with an old name does not compare equal to its new-name twin, because the config keeps what the caller wrote.

Current evidence: CPU tier 2278 passed; live ClickHouse suite 215 passed (plus the one CREATE USER test this local server cannot run); GPU capture, sink and refusal suites 16 passed, with DeprecationWarning as an error. CI green at 90ef0e8.

@zaoxing
zaoxing requested review from Samfisheryu and a lite review from Copilot September 23, 2026 20:53

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@zaoxing
zaoxing force-pushed the fix/capture-fail-loudly branch from ed7a0cc to abf18c2 Compare September 23, 2026 23:26
@zaoxing
zaoxing force-pushed the feat/storage-choice-names branch from 045f399 to 33d78a9 Compare September 23, 2026 23:30
@zaoxing
zaoxing force-pushed the fix/capture-fail-loudly branch from abf18c2 to 9419de0 Compare September 24, 2026 14:07
@zaoxing
zaoxing force-pushed the feat/storage-choice-names branch from 33d78a9 to 90ef0e8 Compare September 24, 2026 14:07
@zaoxing
zaoxing force-pushed the fix/capture-fail-loudly branch from 9419de0 to a7cc4e3 Compare September 24, 2026 14:47
The user's storage choice now has exactly three values:

- "in-memory": records delivered in memory. Until its consumer interface
  exists this is the ClickHouseRecordSink path, which needs a host engine.
- "persistent": the native capture storage path, object store + catalog +
  ClickHouse.
- "none": capture off entirely. The engine allocates no ring, so none of the
  ring's pinned staging or payload memory. A record runtime, a host engine,
  an explicit ring_config and adapter attachment are refused, each with a
  message that names the two choices that capture.

dmi.config.USER_STORAGE_CHOICES lists them. "auto" stays the unset default,
the inference every caller made before the field existed; the configurator
will always emit one of the three. The earlier names still work, with a
DeprecationWarning that names the replacement: "native" means in-memory and
"capture" means persistent. MonitoringConfig keeps whatever the caller wrote,
so an integration that reads the field back still sees its own value. The
engine acts on canonical_storage_backend.

"none" used to mean capture and transport with no persistence: the ring
still ran, and every record then failed at flush_and_wait with "record sink
is not configured". Nothing depended on that, and it is now a clear refusal
up front. The one test that expected an adapter to attach under "none" now
expects the refusal.

Docs: the v1 contract lists the three choices, the aliases and what "none"
refuses; the capture-storage design doc uses the new names.

Tests: a new tests/test_storage_backend_choices.py (13 cpu tests) covers the
three choices, auto as the unset default, the aliases and their warning, the
unknown-value message, and none allocating no ring and refusing a ring_config,
a record runtime, a host engine and attachment. It failed at import before
the change. The existing suites moved to the new names. CPU tier: 2238 passed,
1 skipped (no CUDA device), 0 deprecation warnings. GPU (RTX 4090) capture
storage, sink ring and record-ring refusal suites: 29 passed with
DeprecationWarning as an error, so no test still uses an old name.

Claude-Session: https://claude.ai/code/session_01PcY9QS1FAehHTzkjdpvN6Y
… the engine

Two findings from an independent review:

- storage_backend="none" skipped only the constructor's ring. The public
  enable_ring_transport(), which the adapter docs point callers at, still
  allocated one and made it globally active. It now refuses under "none"
  with the same message as create_record_runtime.
- Nothing tested that a deprecated name reaches the engine: reverting the
  engine's mapping to the raw field left all 85 related tests green. The
  mapping is now a module-level canonical_storage_backend() the engine
  applies to whatever config it is given, so a duck-typed config carrying
  "native" or "capture" is checked too (before, it slipped past every
  check). New tests drive the engine with each old name: "native" without
  a host is refused, "capture" with a host is refused, "capture" with a sink
  config resolves to "persistent", and a duck-typed "native" is refused.
  With the old raw read put back, 3 of them fail.

CPU tier: 2242 passed, 1 skipped (no CUDA device). GPU (RTX 4090) capture
storage, sink ring, record-ring refusal and storage-choice suites: 33
passed with DeprecationWarning as an error.

Claude-Session: https://claude.ai/code/session_01PcY9QS1FAehHTzkjdpvN6Y
…and errors

Three smaller findings from the independent review:

- canonical_storage_backend returned a plain str, so every literal
  comparison in the engine and adapters went unchecked by a type checker.
  It now returns CanonicalStorageBackend, a Literal of the four names the
  engine acts on.
- The deprecation warning used stacklevel=3, which points at the caller
  only for direct construction. Through dataclasses.replace() -- which the
  configurator's _install_schedule uses -- it was attributed to
  dataclasses.py. The level is now computed by walking past this module,
  the generated __init__ and dataclasses.py, so both routes name the
  caller's line.
- A refusal quoted the canonical name even when the caller wrote the old
  one, so "native" produced an error about 'in-memory'. Refusals now read
  "config.storage_backend='native' (now 'in-memory') ...".

Tests: three new ones in test_storage_backend_choices.py, each red first.
With the storage, engine and wiring suites, 72 passed.

Claude-Session: https://claude.ai/code/session_01PcY9QS1FAehHTzkjdpvN6Y
The design doc's comparison table still headed its columns "native path"
and "capture path", and described the persistent side as the Python
reference sink, "explicitly reference-only". Since the native pack writer
became the default and the in-process storage service landed, that read as
"persistent = Python reference-only". The table now names both paths by
their storage choice and describes the native writer and storage service as
production, the Python sink as the reference and rollback.

The v1 contract's list of refusals left out "none" with a ring_config
(ValueError at construction) and did not say that "none" refuses
create_record_runtime and enable_ring_transport with RuntimeError and an
adaptor's attach_model with ConfigurationError. It now lists each with its
exception type.

Claude-Session: https://claude.ai/code/session_01PcY9QS1FAehHTzkjdpvN6Y
…s additions

Under storage_backend="none" the adapter refusal was tested only through
the private helper. A parametrized test now drives all five entry points --
the HF and base attach_model, attach_config, generate_with_monitoring and
generate_greedy_with_monitoring -- and checks each raises "capture is off"
before the model runs. It passed first (the behaviour was right, the
coverage missing); with "none" taken out of the helper's refusal, all five
fail.

#146's later commits added a test and a v1 sentence that still spelled the
persistent backend "capture"; both now say "persistent".

Claude-Session: https://claude.ai/code/session_01PcY9QS1FAehHTzkjdpvN6Y
@zaoxing
zaoxing force-pushed the feat/storage-choice-names branch from 90ef0e8 to 67a07b8 Compare September 24, 2026 14:48
@zaoxing
zaoxing changed the base branch from fix/capture-fail-loudly to main September 24, 2026 14:48
@zaoxing
zaoxing merged commit 240c594 into main Sep 24, 2026
4 checks passed
@zaoxing
zaoxing deleted the feat/storage-choice-names branch September 24, 2026 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants