Skip to content

perf(py): normalise the patch blend per share, not by an accumulated weight - #69

Merged
vboussot merged 2 commits into
mainfrom
feat/separable-weight-sum
Jul 29, 2026
Merged

perf(py): normalise the patch blend per share, not by an accumulated weight#69
vboussot merged 2 commits into
mainfrom
feat/separable-weight-sum

Conversation

@vboussot

@vboussot vboussot commented Jul 28, 2026

Copy link
Copy Markdown
Member

What

Each patch is blended in already carrying its fraction of the blend weight, w / sum_k w, taken from
a per-axis factorisation. The shares sum to one per voxel by construction, so the spatial-sized
weight buffer and the final division pass over every channel both disappear.

Why it is exact

The patch grid is a full per-axis product and the window is separable by construction, so summing it
over the grid factorises:

sum_p prod_d w_d  ==  prod_d sum_k w_d

The total weight is therefore one vector per axis — 13 KB where a 320-row window of a 1331×1775
volume held 2.8 GB.

Measured

Whole-body CT, 295×259×219, patch [96, 128, 160] — the two regimes that bracket the channel count:

before after
117-class segmentation head, fp16 42.7 s 39.7 s
same, streamed 40.8 s 38.8 s
single-channel synthesis, fp32 2.2 s 2.0 s
same, streamed 3.0 s 2.4 s

The memory saved is the weight buffer, 1/(C+1) of the accumulator, so it depends entirely on the
channel count: negligible (0.8%) on a 117-class head, 25% on a 3-component field, 50% on a single
channel — 0.237 → 0.090 GiB of VRAM on a 128×384×384 case, the final division going with it.

One regime is slower: a single-channel streamed blend on a small volume (5.7 → 12.4 ms), where three
broadcast multiplies cost more than one multiply by a precomputed 3-D window and there is almost no
final division to save. It inverts at application scale (1.26× faster above).

Accuracy

The weight no longer needs a floor. The share is a ratio of comparable quantities and stays in
[0, 1], where the raw product underflows fp16 — a Gaussian corner is ~1e-8 against a 6e-5 smallest
normal — and had to be clamped on both sides of the division to stay recoverable.

  • float32 output unchanged to 3.6e-07; Mean and the unweighted path bit-identical.
  • fp16: 519× closer to a float64 reference on the worst Gaussian corner; the whole-volume std of
    the 117-class case lands 53× nearer its float32 value (0.243624 → 0.244509, reference 0.244526).

Tests

Two tests pin what the change rests on, and both fail on the accumulated form:

  • the accumulated weight factorises into one vector per axis (checked against the naive sum);
  • a float16 blend returns the volume it was cut from, borders included.

Existing suite green (ruff, ruff format, mypy, bandit, tests/unit).

Summary by CodeRabbit

  • Performance

    • Improved overlap-blended patch assembly with lower intermediate memory usage.
    • Streaming assembly now blends patches more efficiently while preserving correct overlap accumulation.
  • Bug Fixes

    • Enhanced reconstruction accuracy for overlapping patches, including float16 inputs and tapered window borders.
    • Maintained correct blending across common window types, including cosine and gaussian.
  • Tests

    • Added unit tests validating blending-weight behavior and numeric reconstruction accuracy.

…weight

Each patch is now blended in already carrying its fraction of the blend weight,
w / sum_k w, taken from a per-axis factorisation. The shares sum to one per voxel
by construction, so the spatial-sized weight buffer and the final division pass
over every channel both disappear.

The factorisation is exact, not an approximation: the patch grid is a full
per-axis product and the window is separable by construction, so
sum_p prod_d w_d == prod_d sum_k w_d. The total weight is therefore one vector
per axis -- 13 KB where a 320-row window of a 1331x1775 volume held 2.8 GB.

Measured on a whole-body CT (295x259x219, patch [96,128,160]), the two regimes
that bracket the channel count:

  117-class segmentation head, fp16   42.7s -> 39.7s    RSS 6.07 -> 6.02 GiB
  single-channel synthesis, fp32       2.2s ->  2.0s
  single-channel, streamed             3.0s ->  2.4s

The memory saved is the weight buffer, 1/(C+1) of the accumulator: negligible on
a 117-class head, 25% on a 3-component field, 50% on a single channel (0.237 ->
0.090 GiB of VRAM on a 128x384x384 case, the final division going with it).

It is also more accurate in low precision. The weight no longer needs a floor:
the share is a ratio of comparable quantities and stays in [0, 1], where the raw
product underflows fp16 (a Gaussian corner is ~1e-8 against a 6e-5 smallest
normal) and had to be clamped on both sides of the division. Against a float64
reference the assembled volume is 519x closer on that corner, and the fp16
whole-volume std lands 53x nearer its float32 value. float32 output is unchanged
to 3.6e-07, and Mean and the unweighted path are bit-identical.

Two tests pin what the change rests on: that the accumulated weight factorises
into one vector per axis, and that a float16 blend returns the volume it was cut
from, borders included -- both fail on the accumulated form.
@coderabbitai

coderabbitai Bot commented Jul 28, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19057c02-fb1b-470c-8081-c19e93602ec5

📥 Commits

Reviewing files that changed from the base of the PR and between 64fdbd7 and 3dd1323.

📒 Files selected for processing (1)
  • konfai/data/patching.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • konfai/data/patching.py

📝 Walkthrough

Walkthrough

Patch blending now derives separable per-axis geometry and applies blend shares during accumulation. Full-volume weight buffers and final normalization were removed from regular and streaming accumulators, with tests covering factorization and float16 reconstruction.

Changes

Patch blending refactor

Layer / File(s) Summary
Blend geometry and factorization
konfai/data/patching.py, tests/unit/test_patching.py
PathCombine stores per-axis windows, and accumulator geometry computes cached multiplicative shares. Tests verify that per-axis totals reconstruct the full blend-weight tensor.
Incremental and streamed accumulation
konfai/data/patching.py, tests/unit/test_patching.py
Accumulator and StreamingAccumulator crop and blend patches directly without weight-sum buffers or final division; float16 reconstruction is tested with Cosinus and Gaussian windows.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant PatchInput
  participant Accumulator
  participant ResultBuffer
  PatchInput->>Accumulator: add cropped patch
  Accumulator->>Accumulator: compute blend share
  Accumulator->>ResultBuffer: add share-weighted patch
  Accumulator->>ResultBuffer: assemble without weight normalization
Loading

Poem

A rabbit hops through windows bright,
Shares each patch with balance right.
No weight sums hide below,
Float16 borders softly glow—
The whole volume springs to light! 🐇

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the change, rationale, metrics, and tests, but it does not follow the required template sections and checklist. Rewrite it using the template headings: Description, Related issues, Type of change, How has this been tested?, Checklist, and Breaking changes & migration.
Docstring Coverage ⚠️ Warning Docstring coverage is 53.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, specific, and accurately summarizes the main blending normalization change.
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.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/separable-weight-sum

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
konfai/data/patching.py (1)

273-289: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the blending docs and drop the dead weight path
weight() and the Mean/Cosinus/Gaussian comments still describe the old weight_sum normalization; rewrite them to match the current share-based assembly. PathCombine.weight()/__call__() also have no in-repo callers, so remove the dead path and _data_per_device cache if nothing external depends on them.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@konfai/data/patching.py` around lines 273 - 289, Update the blending
documentation and comments in weight() and the Mean, Cosinus, and Gaussian
implementations to describe the current share-based assembly rather than
weight_sum normalization. Remove PathCombine.weight(), PathCombine.__call__(),
and the _data_per_device cache, provided repository-wide usage confirms no
callers or dependencies remain.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@konfai/data/patching.py`:
- Around line 273-289: Update the blending documentation and comments in
weight() and the Mean, Cosinus, and Gaussian implementations to describe the
current share-based assembly rather than weight_sum normalization. Remove
PathCombine.weight(), PathCombine.__call__(), and the _data_per_device cache,
provided repository-wide usage confirms no callers or dependencies remain.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e22f4de9-d919-4755-a78a-1181d6af9770

📥 Commits

Reviewing files that changed from the base of the PR and between 04c3269 and 64fdbd7.

📒 Files selected for processing (2)
  • konfai/data/patching.py
  • tests/unit/test_patching.py

@vboussot
vboussot merged commit 6d1ddb0 into main Jul 29, 2026
34 checks passed
@vboussot
vboussot deleted the feat/separable-weight-sum branch July 29, 2026 15:21
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