Skip to content

feat(compression): add-encoding-partition Phase 1 — API surface + scaffolding - #107

Merged
jascal merged 2 commits into
mainfrom
impl/add-encoding-partition
May 21, 2026
Merged

feat(compression): add-encoding-partition Phase 1 — API surface + scaffolding#107
jascal merged 2 commits into
mainfrom
impl/add-encoding-partition

Conversation

@jascal

@jascal jascal commented May 21, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the proposal merged today as PR #106. Phase 1 locks the API surface and ships the scaffolding; Phase 2 (per-block dispatch in Compressor.apply) is a separate follow-up impl PR.

After this lands + polygram tags v0.13.0, sae-forge's add-block-structured-sae can bump its pin and begin Phase 2 of its own change.

What ships

polygram/compression/partition.py (new, ~325 lines)

  • BlockSpec dataclass — frozen, custom __hash__ (since encoding_kwargs is a dict). Per-family validator registry _BLOCK_SPEC_KWARG_VALIDATORS keeps the per-family logic out of __post_init__'s branch chain — adding Rung6+ is one new validator + one registry entry.
  • PartitionCoverageError (subclasses ValueError).
  • validate_partition_coverage(partition, n_features_input=...) — disjointness + completeness checks; error message names up to 10 offending feature ids.
  • make_default_block(...) — 'default + heavy override' helper.

polygram/compression/report.py

  • SCHEMA_VERSION 2 → 3.
  • MAX_CLUSTERS_PER_BLOCK = 10_000 (global cluster-id namespace cap; Decision 2 in design.md).
  • New BlockReport dataclass with per-block diagnostics.
  • CompressionReport.blocks: tuple[BlockReport, ...] | None field.
  • from_json defaults blocks=None for v2 payloads (back-compat).
  • Equality + hash include blocks. _block_to_dict, _block_from_dict, _blocks_eq helpers added.

polygram/compression/compressor.py

Compressor.apply now refuses encoding_partition with a clear NotImplementedError naming the Phase 2 follow-up. Refusal happens before any I/O so the input SAE checkpoint isn't even read. This is load-bearing: without it, downstream consumers would silently get a single-encoding compression where a partitioned one was requested.

polygram/config.py

CompressionConfig.encoding_partition: tuple | None = None. __post_init__ type/membership/non-empty validation. The locked surface is tuple (NOT list — required for the frozen=True hashable contract).

polygram/sae_import.py

from_sae_lens(..., encoding_partition=...) kwarg accepted + type-validated.

Public exports

From polygram.compression: BlockSpec, BlockReport, PartitionCoverageError, make_default_block, validate_partition_coverage.

Tests (33 new cases)

§ tests
9.1 BlockSpec validation 11 cases — encoding_class membership, per-family kwargs (Rung5, HEA_Rung2, MPSRung1 extras), empty feature_ids, duplicates, negative ids, empty block_id, hashability
9.2 Partition coverage 5 cases — disjoint+complete passes, overlap names duplicates, missing names holes, pure-extras names extras, negative n_features_input rejected
9.5 make_default_block 4 cases — covers-all-except-excluded, no-exclusions, refuses-empty-result, heavy-override pattern validates
9.4 CompressionConfig 5 cases — default None, accepts tuple, rejects list, rejects non-BlockSpec, rejects empty tuple
9.4 CompressionReport 8 cases — SCHEMA_VERSION == 3, blocks default None, blocks round-trip, v2-payload back-compat load, v3 + blocks=None round-trip, blocks field-by-field equality, MAX_CLUSTERS_PER_BLOCK constant

Suite: 1037 → 1070 (+33). Ruff clean.

Phase 2 follow-up (separate change)

Per-block dispatch in Compressor.apply:

  1. For each block: slice W_dec/W_enc/b_enc to the block's feature_ids.
  2. Build per-block sub-CompressionConfig from the BlockSpec.
  3. Run the existing compress-strategy pipeline on each sub-state.
  4. Stitch per-block results back into a single output SAE (single-tensor-per-key safetensors; per-block structure in the sidecar report only).
  5. Aggregate per-block diagnostics into a top-level CompressionReport with blocks populated.

That work is significant (~150 LOC + the per-block test smoke matrix) and warrants its own PR.

Coordination with sae-forge

After this lands and polygram tags v0.13.0:

  • sae-forge can bump polygram>=0.13.0 pin.
  • add-block-structured-sae's Phase 0.1 task clears.
  • Phase 2 (per-block dispatch in auto_materialise.py, sweep.py, forge.py) can begin against the locked API surface.

Test plan

  • 33 new tests pass.
  • Full polygram suite: 1037 → 1070 (no regressions).
  • Ruff clean.
  • Smoke tests of all new exports (BlockSpec, validate_partition_coverage, make_default_block, Compressor.apply refusal).
  • CI verifies on Linux runners.

🤖 Generated with Claude Code

jascal and others added 2 commits May 21, 2026 09:00
…ffolding

Implements the proposal merged today as PR #106. Phase 1 locks the
API surface and ships the scaffolding (BlockSpec, validation,
CompressionConfig.encoding_partition field, CompressionReport.blocks
schema v2->v3, public exports). Phase 2 (the actual per-block
dispatch in Compressor.apply) is filed as a follow-up; the current
apply() raises NotImplementedError when partitioned configs arrive
so downstream consumers don't silently get a single-encoding
compression where a partitioned one was requested.

What ships

  polygram/compression/partition.py (new, ~325 lines)
    BlockSpec dataclass — frozen, custom __hash__ (since
      encoding_kwargs is a dict). Per-family validator registry
      _BLOCK_SPEC_KWARG_VALIDATORS keeps the per-family logic out of
      __post_init__'s branch chain — adding Rung6+ is one new
      validator + one registry entry.
    PartitionCoverageError (subclasses ValueError).
    validate_partition_coverage(partition, n_features_input=...) —
      disjointness + completeness checks; error message names up to
      10 offending feature ids.
    make_default_block(...) — 'default + heavy override' helper.

  polygram/compression/report.py
    SCHEMA_VERSION 2 -> 3.
    MAX_CLUSTERS_PER_BLOCK = 10_000 (global cluster-id namespace cap;
      Decision 2 in the design.md).
    BlockReport dataclass with per-block diagnostics.
    CompressionReport.blocks: tuple[BlockReport, ...] | None field.
    from_json defaults blocks=None for v2 payloads (back-compat).
    Equality + hash include blocks. _block_to_dict, _block_from_dict,
    _blocks_eq helpers added.

  polygram/compression/compressor.py
    Compressor.apply now refuses encoding_partition with a clear
    NotImplementedError naming the Phase 2 follow-up. Refusal happens
    before any I/O so the input SAE checkpoint isn't even read.

  polygram/config.py
    CompressionConfig.encoding_partition: tuple | None = None.
    __post_init__ type/membership/non-empty validation. The locked
    surface is tuple (NOT list — required for frozen-hashable contract).

  polygram/sae_import.py
    from_sae_lens(..., encoding_partition=...) kwarg accepted +
    type-validated. No-op routing (from_sae_lens builds Dictionaries,
    not compressed checkpoints; the partition is consumed at
    Compressor.apply time).

  polygram/compression/__init__.py
    New public exports: BlockSpec, BlockReport, PartitionCoverageError,
    make_default_block, validate_partition_coverage.

Tests (33 new cases)

  tests/compression/test_add_encoding_partition.py
    §9.1 BlockSpec validation (11 cases) — encoding_class membership,
      per-family kwargs (Rung5, HEA_Rung2, MPSRung1 extras), empty
      feature_ids, duplicates, negative ids, empty block_id,
      hashability.
    §9.2 Partition coverage (5 cases) — disjoint+complete passes,
      overlap names duplicates, missing names holes, pure-extras
      names extras, negative n_features_input rejected.
    §9.5 make_default_block (4 cases) — covers-all-except-excluded,
      no-exclusions, refuses-empty-result, heavy-override pattern
      validates.
    §9.4 CompressionConfig.encoding_partition (5 cases) — default
      None, accepts tuple, rejects list, rejects non-BlockSpec,
      rejects empty tuple.
    §9.4 CompressionReport schema (8 cases) — SCHEMA_VERSION == 3,
      blocks default None, blocks round-trip, v2-payload back-compat
      load, v3 + blocks=None round-trip, blocks field-by-field
      equality, MAX_CLUSTERS_PER_BLOCK constant.

  Suite: 1037 -> 1070 (+33). Ruff clean.

Coordination

  This change locks the API surface for sae-forge's add-block-
  structured-sae (Phase 0 of which explicitly cites these names).
  After this lands + polygram tags v0.13.0, sae-forge can bump its
  polygram>=0.13.0 pin and begin Phase 2 of its own change.

  The Phase 2 follow-up for THIS change (per-block dispatch in
  Compressor.apply + stitching, plus the BlockReport population from
  per-block compress sub-runs) is a separate impl PR. Filing it
  follows once Phase 1 is merged + tagged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mple

Addresses PR #107 review:

  - Compressor.apply's NotImplementedError now points at the openspec
    change directory (post-archive path) so downstream devs hitting
    the refusal can navigate to the Phase 1 design + the eventual
    Phase 2 impl PR directly.

  - partition.py module docstring gains a 'Usage example' block
    showing the canonical 'default + heavy override' pattern end
    to end (BlockSpec + make_default_block + validate_partition_coverage
    + CompressionConfig). Demonstrates the Phase 1 surface in
    runnable form.

Non-functional polish; no behavioural change. 33/33 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jascal
jascal merged commit 6d94df6 into main May 21, 2026
2 checks passed
@jascal
jascal deleted the impl/add-encoding-partition branch May 21, 2026 13:54
jascal added a commit that referenced this pull request May 21, 2026
…#108)

Tags the polygram release that ships add-encoding-partition Phase 1
(#107) and archives the now-completed openspec change directory.

Wave C unblocker: this release locks the API surface (BlockSpec,
CompressionConfig.encoding_partition, CompressionReport.blocks,
from_sae_lens kwarg) that sae-forge's add-block-structured-sae
proposal explicitly cites as its Phase 0 prerequisite. Once this
tag publishes, sae-forge can bump its polygram>=0.13.0 pin and
begin Phase 2 of its own change.

Phase 2 (the actual per-block dispatch in Compressor.apply) is a
separate impl follow-up filed against this proposal — not in
0.13.0.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jascal added a commit that referenced this pull request May 21, 2026
#109)

Wires the per-block compress + stitch logic deferred from Phase 1
(PR #107). Compressor.apply now actually runs per-block dispatch
when an encoding_partition is supplied, instead of refusing with
NotImplementedError.

What ships

  polygram/compression/compressor.py (~280 new lines)

    Five new private helpers for the per-block path:

      _partition_global_plan_into_blocks(global_plan, partition)
        Splits a globally-computed CompressionPlan into per-block
        cluster lists. Clusters whose members span more than one
        block are DROPPED (their features end up as singletons —
        cross-block merges are semantically invalid since the two
        blocks use different encodings). Returns (per_block_clusters,
        n_cross_block_dropped).

      _build_local_plan_for_block(block_clusters, block)
        Re-indexes a block's ClusterPlan members + representative
        + zeroed to LOCAL indices into the block's sliced W_dec
        (positions 0..len(block.feature_ids)-1). Cluster ids stay
        GLOBAL so the BlockReport's cluster_assignments and the
        top-level CompressionReport.plan reference the same id space.

      _slice_state_to_block(source_state, block)
        Returns a copy of source_state with W_dec/W_enc/b_enc
        column-sliced to block.feature_ids. b_dec is shared (invariant
        under feature-axis slicing; strategies don't touch it).

      _stitch_block_into_state(global_state, sub_rewritten, block)
        Writes per-block rewritten rows back into global_state at
        the positions named by block.feature_ids. Mutates
        global_state in place.

      _build_block_report(block, block_clusters, sub_rewritten_w_dec,
                          sub_source_w_dec, sub_merged_norms)
        Builds the BlockReport for one block with per-feature
        cluster_assignments (LOCAL cluster ids in
        [0, n_clusters_in_block); -1 for features not in any
        cluster). v1 leaves per-block rank_ratio/post_A/forge_mse
        as None — those diagnostic floats are a separate enhancement.

      _apply_partitioned(source_state, global_plan, partition, ...)
        The top-level driver. Calls the helpers above per block,
        stitches the rewritten state, builds block_reports.
        Returns (rewritten_state, merged_norms, block_reports,
        n_cross_block_dropped).

    Compressor.apply integration:
      - Replaces the Phase 1 NotImplementedError refusal with the
        actual per-block dispatch path.
      - Coverage validation runs immediately (before any I/O) so an
        under- or over-specified partition fails fast.
      - The top-level CompressionReport's plan is rebuilt to drop
        cross-block clusters; the apply()'s downstream rank_ratio /
        post_A / scale_compression_ratio diagnostics then reflect
        the cross-block-dropped clusters, not the original global ones.
      - CompressionReport.blocks is populated with the BlockReport
        tuple.
      - Defensive: when plan.feature_ids is empty (all clusters
        cross-block dropped), the downstream rebuild seeds from the
        lowest-fid features so the rebuilt Dictionary still surfaces
        as a debugging aid.

    BlockReport.n_features_kept semantic aligned with top-level
    CompressionReport.n_features_kept: count of cluster representatives
    (= n_clusters), not all surviving features in the block.
    Singletons stay singletons; only multi-feature clusters contribute
    to the kept count.

Tests (9 new cases, 1 Phase 1 case updated)

  tests/compression/test_encoding_partition_phase2.py (new)
    - test_single_block_partition_runs_strategy_correctly: 1-block
      partition covering all features matches the top-level
      CompressionReport's counts.
    - test_two_block_partition_stitches_correctly: 2 blocks with
      intra-block clusters; per-block + top-level counts agree.
    - test_two_block_partition_zeroes_correct_rows_in_output:
      verifies the stitched output W_dec has the right rows zeroed
      (uses explicit n_fires for deterministic rep selection).
    - test_cross_block_clusters_are_dropped: confirmed pair
      bridging two blocks is dropped; both features survive as
      singletons.
    - test_coverage_validation_fires_on_incomplete_partition:
      PartitionCoverageError fires at apply() time before I/O.
    - test_coverage_validation_fires_on_overlapping_partition:
      overlap raises with the duplicate id named.
    - test_block_report_cluster_assignments_local_to_block:
      cluster_assignments uses local indices 0..n_clusters_in_block-1
      with -1 sentinels for features not in any cluster.
    - test_full_compression_report_with_blocks_round_trips:
      v3 report with blocks round-trips through to_json / from_json.
    - test_no_partition_uses_single_encoding_path: regression
      guard — when encoding_partition=None the historical path
      runs and CompressionReport.blocks is None.

  tests/compression/test_add_encoding_partition.py:
    test_compressor_apply_refuses_encoding_partition (Phase 1)
    replaced with test_compressor_apply_runs_partition_path_in_phase_2
    — pins the Phase 1 → Phase 2 transition; the Phase 1
    NotImplementedError refusal is gone.

Tests: 1070 → 1079 (+9). Full suite green. Ruff clean.

Phase 2 v1 caveats (documented enhancements, not blockers)

  - Per-block rank_ratio / post_A / forge_mse: deferred to a
    separate enhancement. Each block's diagnostic floats currently
    serialise as None in the BlockReport. The top-level
    CompressionReport's rank_ratio / post_A still compute (across
    the dropped-cross-block plan).
  - merged_norms aggregation: the per-block sub_merged_norms maps
    use the GLOBAL cluster_id (preserved through the local-plan
    construction in _build_local_plan_for_block), so the top-level
    merged_norms dict is a clean union of per-block dicts. No
    namespace collisions.
  - merge strategy: works the same as zero — the strategy is
    called per-block on each sub-state, so merge_mode is honored.
    Tests above cover only zero strategy explicitly; merge support
    is implicit through the existing dispatch_strategy call.

Coordination

  After this lands, polygram tags v0.14.0 → sae-forge can begin
  add-block-structured-sae Phase 2 (per-block dispatch in
  auto_materialise.py, sweep.py, forge.py) against the now-functional
  partition surface.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant