Skip to content

refactor: split config resolution from validation (#13) - #19

Merged
rgutzen merged 13 commits into
Lindsay-Lab:mainfrom
rgutzen:refactor/split-config-resolution
Sep 9, 2026
Merged

refactor: split config resolution from validation (#13)#19
rgutzen merged 13 commits into
Lindsay-Lab:mainfrom
rgutzen:refactor/split-config-resolution

Conversation

@rgutzen

@rgutzen rgutzen commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Introduces a pure, testable config-resolution layer that operates on plain dicts
before pydantic validation. Fixes issue #13.

Changes

  • New module: dynvision/params/resolution.py (670 lines) with a single
    interface: resolve(sources, schema) -> ResolvedConfig. Pure: no pydantic, no
    torch, no external dynvision imports except the equally dependency-free
    provenance.py.

  • Adapter: CompositeParams.build_config_schema() projects the pydantic class
    graph into plain-data ConfigSchema — component fields, aliases, mode name,
    preprocessors, unscoped handler. The only place that reads model_fields.

  • Three explicit stages in from_cli_and_config: gather (I/O) → resolve (pure)
    → validate. _instantiate_resolved() is the isolated validation half.

  • Extracted ProvenanceRecord/ParamsDict into a dependency-free
    provenance.py so the resolution layer can use them. Re-exported from
    base_params to keep every import path working.

  • Line count: composite_params.py shrank from 1,107 → 743 lines.
    Resolution logic is now in one module instead of scattered across four.

Regression: the c10::Half crash

The alias precision → trainer.precision was deleted before other components saw
it, so DataParams.precision silently stayed None and mismatched its dtype vs.
the model's. No test could reach that intermediate state because the only way to
inspect the dict was to construct a fully valid params object.

Now test_alias_diverts_unscoped_key_away_from_sibling_components directly asserts
this shape on a plain dict, without torch or a dataset.

Tests

  • 47 new tests on ConfigSchema, ConfigSource, resolve(), and the helper
    functions. All use plain dicts; all pass without pydantic or torch.
  • Architectural invariant test (test_resolution_layer_has_no_validation_dependencies)
    fails the build if anyone reintroduces a pydantic/torch import.
  • Full suite: 238 passed / 3 skipped (191 baseline + 47 new), same 3 pre-existing
    ffcv failures.
  • Every private classmethod delegation keeps working; 10+ existing test call sites
    require zero changes.

Stacked on #18

This branch was rebased onto refactor/dtype-policy (PR #18) because issue #13
explicitly generalizes the pattern that branch established, and the motivating
bug's fix lives there. Should merge after #18.

Outstanding: mkdocs build

mkdocs is not installed in the dynvision conda env. Code structure was verified
manually (code-fence parity, referenced files exist, planning docs covered by
not_in_nav rule), and the doc's worked example was executed to verify the output.
Actual build should be confirmed in CI.

Related issues

Your Name and others added 8 commits August 28, 2026 17:28
Fix the test-time dtype mismatch (Half input vs float32 conv) by making
TrainerParams.precision the single source of truth for dtype resolution.

- Add dynvision/utils/dtype_policy.py: canonical PRECISION_TO_DTYPE map
  (stored-parameter semantics), resolve_dtype (warn + float32 default),
  DtypePolicy dataclass, and shared coordinate_component_dtypes helper.
- Route get_effective_dtype_from_precision and DtypeDeviceCoordinator
  through the shared resolver; replace silent float16 fallbacks with
  warn + float32.
- Delete DataParams.precision and dead ModelParams.target_dtype.
- Apply the same coordinate_component_dtypes validator to TestingParams
  (removing the 'precision' alias that swallowed unscoped precision and
  caused data to derive float16).
- Default dataloaders to float32.

Regression tests in tests/params/test_dtype_policy.py.
…-free module

Prerequisite for a pydantic-free resolution layer (Lindsay-Lab#13). Both names are
re-exported from base_params so existing import paths keep working.
…ce (Lindsay-Lab#13)

Introduces ConfigSchema / ConfigSource / ResolvedConfig and a resolve()
function that turns labelled parameter sources into per-component dicts
without any pydantic or torch dependency.
…indsay-Lab#13)

from_cli_and_config now runs three explicit stages: gather (I/O), resolve
(pure), validate. build_config_schema() is the adapter that projects the
pydantic class graph onto a plain ConfigSchema, and _instantiate_resolved()
is the isolated validation half.

All private phase helpers are retained as thin delegations, so the existing
test call sites and the _handle_unscoped_param / preprocessor override points
in Init/Testing/TrainingParams keep working unchanged.

composite_params.py: 1107 -> 743 lines.
Updates the parameter-processing developer guide to describe the three
stages (gather / resolve / validate), the resolution-layer types, and the
alias-shadowing caveat that caused the c10::Half crash. Marks the planning
doc implemented and records deviations.
Copilot AI lite review requested due to automatic review settings September 9, 2026 06:41

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.

🟡 Changes recommended

dynvision/base/coordination.py currently mis-normalizes "bfloat16"/"torch.bfloat16" in map_dtype(), causing explicit bfloat16 targets to fall back to float32.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR implements issue #13 by splitting config resolution (precedence/alias/mode routing over plain dicts) from pydantic validation, introducing a pure resolve(sources, schema) -> ResolvedConfig seam. It also includes the stacked dtype-policy work that centralizes Lightning precision → stored-parameter torch.dtype mapping and aligns data/trainer dtype behavior in both training and testing contexts.

Changes:

  • Added a dependency-free resolution layer (dynvision/params/resolution.py) plus shared provenance primitives (dynvision/params/provenance.py), and rewired CompositeParams to run gather → resolve → validate.
  • Centralized precision→dtype logic in dynvision/utils/dtype_policy.py, updated params/components to use it, and removed legacy dtype/precision fields/aliases that caused the c10::Half mismatch.
  • Added extensive unit tests for resolution/dtype behavior and updated developer docs to reflect the new architecture.
File summaries
File Description
tests/params/test_dtype_policy.py Adds regression + unit tests for centralized precision→dtype mapping and coordination behavior.
tests/params/test_config_resolution.py Adds pure-dict resolution tests and enforces “no validation deps” invariant for the resolver.
pyproject.toml Updates scikit-image requirement and comments out ffcv dependency.
dynvision/utils/torch_utils.py Makes get_effective_dtype_from_precision delegate to the central dtype policy and return torch.dtype.
dynvision/utils/dtype_policy.py Introduces the canonical precision→dtype map + policy helpers + shared dtype coordination routine.
dynvision/utils/init.py Re-exports dtype policy symbols from dynvision.utils.
dynvision/params/training_params.py Uses shared dtype-coordination helper during validation.
dynvision/params/trainer_params.py Stores/returns effective dtype as torch.dtype derived via the shared resolver.
dynvision/params/testing_params.py Removes the problematic precision alias; adds dtype coordination validator and coordinated-dtype accessor.
dynvision/params/resolution.py New pure resolution implementation (alias/scope/source precedence, provenance propagation, preprocessors).
dynvision/params/provenance.py New dependency-free home for ProvenanceRecord and ParamsDict.
dynvision/params/model_params.py Removes target_dtype field.
dynvision/params/data_params.py Removes precision field/validator and legacy dtype derivation; adjusts dtype defaults/fallbacks.
dynvision/params/composite_params.py Introduces schema adapter + integrates resolution layer; keeps legacy helpers as delegating wrappers.
dynvision/params/base_params.py Re-exports provenance primitives from the new dependency-free module.
dynvision/data/ffcv_dataloader.py Changes default dtype from float16 to float32.
dynvision/data/dataloader.py Changes default dtype from float16 to float32.
dynvision/configs/config_defaults.yaml Removes target_dtype default entry.
dynvision/base/coordination.py Routes dtype mapping through resolve_dtype and updates float16 fallbacks to float32.
docs/development/planning/dtype-handling-refactor.md Adds planning/design doc for the dtype-policy refactor.
docs/development/planning/config-resolution-refactor.md Adds planning/design doc for the resolution/validation split and recorded deviations.
docs/development/guides/parameter-processing.md Updates developer guide to describe gather → resolve → validate and the new resolver interfaces.
Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread dynvision/base/coordination.py Outdated
…ional ffcv import

- map_dtype() in coordination.py stripped the substring "float" from all
  spellings, turning "bfloat16"/"torch.bfloat16" into "b16", which
  resolve_dtype() couldn't match and silently fell back to float32.
  Now bfloat16 spellings normalize to "bf16" explicitly before the
  "float" strip is applied to the other spellings.

- DataParams.effective_dtype did a string-keyed dict lookup on self.dtype,
  but validate_dtype already converts self.dtype to torch.dtype, so the
  lookup always missed and silently returned torch.float32 regardless of
  the configured dtype. Now returns self.dtype directly.

- data_params.py and transforms.py had unconditional top-level
  `from ffcv.loader import OrderOption` / `import ffcv.transforms`, but
  ffcv is commented out of pyproject.toml (no wheel / missing system
  deps). This broke every test module that imports dynvision.params or
  dynvision.data.transforms in environments without ffcv installed
  (8 CI import errors). Both imports are now optional: data_params.py
  falls back to a local OrderOption-mirroring IntEnum, and transforms.py
  raises a clear ImportError only if backend="ffcv" is actually
  requested without ffcv installed.

Adds regression tests for both dtype bugs and verifies the ffcv-optional
import path by blocking ffcv at runtime.
ffcv is an optional dependency (commented out of pyproject.toml — no
wheel / missing system deps), but tests/data/test_transforms.py had no
guard for the tests exercising backend="ffcv". After making the ffcv
imports in dynvision/data/transforms.py and dynvision/params/data_params.py
optional, these tests started failing loudly in CI with the new, correct
ImportError instead of the previous collection-time ModuleNotFoundError
that masked the whole test module.

Add a requires_ffcv skip marker (based on actual ffcv importability) to
the three tests that genuinely need a working ffcv install:
test_parse_bare_module_name_ffcv, test_get_ffcv_train_transforms,
test_ffcv_mnist_train_workflow. Verified clean pass+skip both with ffcv
present and with it blocked at import time.

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.

🟡 Changes recommended

merge_mode_sections() currently contradicts its “input is not modified” contract by using a shallow copy, which can mutate callers’ nested config dicts during mode merges.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

dynvision/params/resolution.py:303

  • merge_mode_sections() says the input config is not modified, but merged = dict(config) is a shallow copy. If config contains nested dicts outside the mode block (e.g., trainer: {…}), the subsequent deep_merge() will mutate those shared nested dict objects in the caller’s input mapping.
  • Files reviewed: 26/26 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines 220 to +224
def effective_dtype(self) -> Optional[torch.dtype]:
"""Get effective dtype without optimization (to avoid circular dependency)."""
# Convert string dtype to torch dtype if specified
if self.dtype is not None:
dtype_map = {
"float16": torch.float16,
"float32": torch.float32,
"float64": torch.float64,
"int8": torch.int8,
"int16": torch.int16,
"int32": torch.int32,
"int64": torch.int64,
}
return dtype_map.get(self.dtype, torch.float16)

return None
# validate_dtype already normalizes self.dtype to a torch.dtype (or
# None), so no further string-keyed lookup is needed here.
return self.dtype
Comment thread dynvision/base/coordination.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@rgutzen

rgutzen commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator Author

@copilot resolve the merge conflicts in this pull request

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.

🟡 Changes recommended

The new resolver currently (1) can leak composite provenance entries for keys not present in the composite base when unscoped_handler routes keys only into components, and (2) merge_mode_sections advertises non-mutation but can mutate nested dicts due to shallow copying.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread dynvision/params/resolution.py Outdated
Comment thread dynvision/params/resolution.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

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.

🔵 Needs a closer look

It refactors core configuration/validation behavior across multiple layers (including precedence and provenance plumbing), so despite strong tests it warrants final human review given its system-wide impact.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@rgutzen
rgutzen merged commit 9474ff4 into Lindsay-Lab:main Sep 9, 2026
6 of 7 checks passed
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.

Split config resolution from config validation in composite_params.py

2 participants