Skip to content

Add opt-in PiD memory optimizations - #9460

Open
JPPhoto wants to merge 13 commits into
invoke-ai:mainfrom
JPPhoto:pid-optimization
Open

Add opt-in PiD memory optimizations#9460
JPPhoto wants to merge 13 commits into
invoke-ai:mainfrom
JPPhoto:pid-optimization

Conversation

@JPPhoto

@JPPhoto JPPhoto commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds opt-in PiD memory optimizations for GPUs with limited VRAM.

When pid_memory_optimization: true is set in invokeai.yaml, PiD uses float32 sampler intermediates and chunks full-resolution PiT activations. The option applies to every supported PiD decoder. It defaults to false, preserving existing behavior.

Chunking is configured per decode call so cached PiD models cannot retain optimization state between requests.

This PR adds backend tests, generated configuration types, the complete generated settings entry, and PiD/low-VRAM documentation.

Related Issues / Discussions

QA Instructions

  1. Add pid_memory_optimization: true to invokeai.yaml.
  2. Restart InvokeAI.
  3. Run a PiD decode and confirm it completes with reduced peak VRAM.
  4. Remove the setting or set it to false, restart, and confirm the original unchunked path is used.

Merge Plan

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

@JPPhoto
JPPhoto requested a review from blessedcoolant as a code owner August 4, 2026 22:17
@JPPhoto JPPhoto added the 6.14.0 label Aug 4, 2026
@JPPhoto JPPhoto moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap Aug 4, 2026
@github-actions github-actions Bot added python PRs that change python files invocations PRs that change invocations backend PRs that change backend files services PRs that change app services frontend PRs that change frontend files python-tests PRs that change python tests docs PRs that change docs labels Aug 4, 2026
@JPPhoto JPPhoto changed the title Add opt-in PiD optimizations Add opt-in PiD memory optimizations Aug 5, 2026
@JPPhoto
JPPhoto force-pushed the pid-optimization branch 3 times, most recently from 47cd9fb to 42097b6 Compare August 5, 2026 17:48
@Pfannkuchensack

Pfannkuchensack commented Aug 6, 2026

Copy link
Copy Markdown
Member

PR #9460 — Add opt-in PiD memory optimizations

  • The freed VRAM stays reserved, so the feature is only half wired. estimate_pid_decode_working_memory (invokeai/backend/pid/decode.py:137) is not flag-aware — it still returns out_h * out_w * 4 * 250 = 3.9 GiB at 2048², while the measured peak with the flag on is 1.5 GiB. The cache takes max(working_mem_bytes, device_working_mem_gb) (invokeai/backend/model_manager/load/model_cache/model_cache.py:1093) and subtracts it from the weight budget. The comment above the constant says exactly what is at stake: "an over-large value ... forces PidNet to partial-load onto the CPU (slow)." On the low-VRAM systems this feature targets, the saving prevents a hard OOM but never becomes weight residency.
  • The documented cost is not the real cost. "at the cost of slower decoding" (invokeai/app/services/config/config_default.py:116 and :224, plus both docs pages) is not observable: 2.78 s both ways at 2048px, median of 3 with warmup. What is real and undocumented is that the output changes. Fix the description and both docs pages.
  • Chunked ≠ unchunked on the target hardware, and the test cannot fail. On CUDA under bf16 autocast at production dimensions, a single PiTBlock gives max|diff| = 1.59e-2 and assert_close fails; the same block on CPU fp32 is bit-identical. test_pit_block_chunked_forward_matches_unchunked_and_bounds_adaln_batch passes only because it runs at pixel_hidden_size=4 / BL=8 / chunk=3 on CPU — both use_autocast parametrizations are CPU-only. End-to-end effect: PSNR 43.2 dB, max|diff| 0.66 on [-1,1], 13.1% of pixels differ by >2 LSB. Add a CUDA + bf16 test at production dimensions with BL >= 2 * chunk_size and either fix the divergence or make the tolerance an explicit, documented contract.
  • Split the fp32 _velocity_to_x0 change out of the memory flag, or justify it. Measured in isolation at 2048²: 288 → 96 MiB, i.e. 192 MiB — against 2.18 GiB from chunking alone (one unchunked PiTBlock peaks at 2864.7 MiB vs 598.4 MiB chunked). It is a precision reduction that saves almost no peak memory but does change the image (PSNR 44.2 dB standalone). The two tests for it only assert which branch is taken — neither measures memory nor bounds the delta.
  • adaLN_modulation(s_cond) is computed twice per chunk (invokeai/backend/pid/_src/networks/pixeldit_official.py:541 and :548), each time discarding most slices. That is ~9.9 TFLOP extra per 2048px decode and the only real basis for the "slower" claim. Return the remaining four slices from _compress_activation_chunk instead.
  • Nothing enforces that the nodes forward the setting. All seven construction sites pass it today (verified by grep), but the tests cover _student_sample_loop, _velocity_to_x0 and config loading — never the invocation layer. An eighth PiD node could silently omit it.
  • The setting leaves no trace on the output. It is a server config value, never a node input, so it never enters graph metadata. The same workflow with the same seed produces different pixels on two servers with nothing recording why.
  • No observability: no log line when the flag is enabled, and _PID_ACTIVATION_CHUNK_SIZE (invokeai/backend/pid/decode.py:31) is hardcoded and unconfigurable — a yaml-only, restart-required knob with zero feedback.
  • Untested paths: batch B > 1 (chunk boundaries then straddle images), the context-parallel (_cp_group) branch, and the feature_indices / discriminator branch of PidNet.forward.

Pfannkuchensack added a commit to JPPhoto/InvokeAI that referenced this pull request Aug 6, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 7, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 7, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 7, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 8, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
@JPPhoto

JPPhoto commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

@Pfannkuchensack Thanks for the detailed review. I addressed all four findings in commit ec48ddf5c7.

  1. The working-memory estimator now accounts for CUDA autocast's temporary bf16 copies of fp32 PiD weights. It receives the loaded model and compute device, sums the fp32 parameter bytes, and applies the term only for CUDA. All PiD invocation nodes now pass this information.

  2. I removed the hard-coded RTX 4090 activation-range assertions. Tests now verify the optimization relationship and separately validate the model-sized autocast-cache term. I also increased the chunked scaling coefficient to avoid under-reserving at the reviewed 3072-4096px resolutions.

  3. I corrected the clamp rationale. The code now explicitly distinguishes the chunk-engagement threshold from the crossover point between the calibrated formulas.

  4. I added a reduced-size end-to-end PiDDecoder.decode(..., pid_memory_optimization=True) test that spies on PiTBlock._forward_chunked and confirms the chunked path is reached.

The documentation now clarifies that activation figures exclude the temporary model-sized autocast copies. Relevant tests (CUDA and non-CUDA) pass locally.

JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 9, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 9, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
@Pfannkuchensack

Copy link
Copy Markdown
Member

Findings

1. High - the bf16 autocast weight-cache term does not exist in production and inflates every PiD working-memory reservation by ~2.6 GiB

invokeai/backend/pid/decode.py:185-193 adds sum(p.numel() * 2 for p in model.parameters() if p.dtype == torch.float32) to the working-memory estimate. For PidNet(FLUX) (1.362 B params) that is 2598 MiB, added on both the optimized and the unoptimized branch (lines 194 and 202-205), i.e. also when pid_memory_optimization is false (the default).

That cache never materializes, because invokeai/backend/model_manager/load/model_loaders/pid_decoder.py:103 ends _load_model with pid_net.requires_grad_(False). PyTorch's cached_cast only caches a lower-precision copy of an fp32 leaf tensor with requires_grad == True, so the shipped configuration retains nothing.

Measured with the installed PiD_res2k_sr4x_official_flux_distill_4step checkpoint, real Gemma-2-2b-it caption embeddings and a real FLUX VAE latent, 4 steps, RTX 4090 / torch 2.7.1+cu128:

Regime                                                     flag off   flag on
requires_grad=True  + autocast cache ON                        6373      4140   <- never occurs
requires_grad=True  + autocast cache OFF                       3762      1530
requires_grad=False + autocast cache ON  (PRODUCTION)          3762      1530
requires_grad=False + autocast cache OFF                       3762      1530

requires_grad=False also survives partial loading: cached_model_with_partial_load.py:306 uses load_state_dict(..., assign=True), which propagates param.requires_grad.

Impact. model_cache.py:1092-1093 floors the reservation at device_working_mem_gb (3072 MiB), which absorbs part of the error, so the practical damage is the delta in the effective reservation max(estimate, 3072 MiB):

                 PR as-is   term removed   actual harm   real peak
1024px  flag off  3598 MiB       3072 MiB       526 MiB     945 MiB
1024px  flag on   3302 MiB       3072 MiB       230 MiB     507 MiB
2048px  flag off  6598 MiB       4000 MiB      2598 MiB    3765 MiB
2048px  flag on   4742 MiB       3072 MiB      1670 MiB    1533 MiB

2048px is the normal case (PiD is a 4x SR decoder; a 512px-source latent produces exactly that). Via model_cache.py:1111 (vram_total_available_to_cache = vram_available_to_process - working_mem_bytes) and a 5196 MiB PidNet, the free VRAM required to keep PidNet resident at 2048px goes from 9.0 GiB to 11.5 GiB with the flag off - a regression against main for users who never enable the setting - and from 8.1 GiB to 9.7 GiB with the flag on, which is the low-VRAM audience the feature targets.

Note the history: the calibration figures introduced in 0a53baaaa2 (509 MiB at 1024px, 1533 MiB at 2048px) match production to within 2 MiB and are correct. The term added in cfa38a01af corrects for a shortfall that only appears when requires_grad is True.

Fix

  • invokeai/backend/pid/decode.py:185-193, 194, 202-205 - drop the term, or gate it on any(p.requires_grad for p in model.parameters()).
  • If dropped: remove the model / device keyword-only parameters (decode.py:161-163), the docstring sentence at decode.py:169-171, and the comment fragments at decode.py:128 and decode.py:133.
  • Remove model=pid_info.model / device=pid_info.compute_device from all seven call sites: flux2_pid_decode.py:213, flux_pid_decode.py:139, pid_upscale.py:183, qwen_image_pid_decode.py:205, sd3_pid_decode.py:136, sdxl_pid_decode.py:181, z_image_pid_decode.py:182.
  • Delete the sentence "CUDA working-memory reservations also include temporary bf16 autocast copies of the float32 PiD weights; those model-sized copies are not included in the activation figures." from docs/src/content/docs/configuration/low-vram-mode.mdx:160 and docs/src/content/docs/features/pid-decode.mdx:78.

Test changes. The current tests pin the defect and must be updated, not merely kept green:

  • tests/backend/pid/test_pid_decode.py:176 (test_working_memory_estimate_includes_cuda_autocast_weight_cache) asserts the term is charged, using torch.nn.Linear(4, 4) with its default requires_grad=True.
  • tests/app/invocations/test_pid_memory_optimization_wiring.py:70-79 hard-requires model= and device= on every node's estimate call.

To expose this issue, add a test that passes a model with requires_grad_(False) applied - the state PiDDecoderLoader._load_model leaves the net in - and asserts that estimate_pid_decode_working_memory(..., model=net, device=torch.device("cuda")) equals the activation-only estimate, i.e. that no bf16 weight-cache bytes are charged when autocast cannot cache.

2. Low - the docs promise a VRAM saving that device_working_mem_gb prevents users from observing

With the term from Finding 1 removed, the optimized estimate is 2144 MiB at 2048px and 704 MiB at 1024px - both below the 3072 MiB device_working_mem_gb floor applied at model_cache.py:1092-1093. The cache therefore still withholds 3 GB, so the measured 1533 MiB peak never turns into usable weight VRAM unless the user also lowers device_working_mem_gb.

docs/src/content/docs/configuration/low-vram-mode.mdx:160 and docs/src/content/docs/features/pid-decode.mdx:78 state the peak drop without this caveat. Add one sentence to the low-VRAM page noting that device_working_mem_gb floors the reservation, so realising the full benefit at 1024px-2048px requires lowering it as well.

Verified - do not change

Measured end-to-end against the installed checkpoint; these all hold and should be left alone:

  • Savings figures. ~3.7 GB -> ~1.5 GB and ~0.9 GB -> ~0.5 GB are accurate: 3765 -> 1533 MiB and 945 -> 507 MiB. The calibration comment at decode.py:141-143 (509 / 1533 MiB) is correct to within 2 MiB.
  • Calibration constants. _PID_DECODE_WORKING_MEMORY_SCALING_CONSTANT = 250, _PID_DECODE_CHUNKED_SCALING_CONSTANT = 120, _PID_DECODE_CHUNKED_FIXED_BYTES = 224 MiB all sit above the real peak with 6-40% margin, at B=1 and B=2.
  • Quality claim. Three real images: PSNR 56.0 / 56.0 / 50.3 dB at 2048px and 52.7 dB at 1024px, against the documented ~43 dB. Worst image: 99.97% of subpixels within 8 levels, 0.0001% above 64. "Visually indistinguishable, but not bit-exact" is accurate and conservative.
  • Speed claim. 2048px with the real net: 3.25-4.17 s off vs 3.24-4.26 s on; isolated chunking cost +4.1%. "Roughly unchanged" holds.
  • Chunking correctness. Chunked and unchunked PiTBlock paths are equivalent modulo reassociation, including chunk boundaries that straddle images (B=2); the mask and CP paths are untouched.

Notes

  • No auth, route, filesystem or subprocess surface is touched; the setting is yaml/env only. No new user-visible frontend strings, so no i18n obligation - the only frontend change is the generated schema.ts / openapi.json field.
  • test_chunking_stays_within_the_documented_tolerance_on_cuda_bf16 is the only test covering the shipped CUDA path and will skip on GPU-less CI, leaving the tolerance contract unverified there.
  • If max_cache_vram_gb is set, model_cache.py:1087-1090 returns before working_mem_bytes is computed, so the whole PiD estimate - and this bug - has no effect for those users.

@JPPhoto

JPPhoto commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

Ignoring finding 1 per our Discord conversation regarding thrashing on this, and continuing work on the rest.

@JPPhoto

JPPhoto commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator Author

@Pfannkuchensack This is ready for another review.

@Pfannkuchensack

Copy link
Copy Markdown
Member

Correction to my 2026-08-08 review, and the corrected numbers

My finding 1 from 2026-08-08 was wrong, and the term added in ec48ddf5c7 should be removed. The cause was in my measurement harness, not in your code: it built the net via build_pid_net(...) and never applied requires_grad_(False). PiDDecoderLoader._load_model does apply it (invokeai/backend/model_manager/load/model_loaders/pid_decoder.py:103), and Torch's cached_cast only caches a bf16 copy of an fp32 leaf tensor with requires_grad == True. So I measured a regime the application never runs in, and the "direct confirmation" I cited - a 256px decode peaking at 2686 MiB - was the artifact itself.

Eight lines, no model needed:

import torch
lin = torch.nn.Linear(8, 8, bias=False).cuda().float()
lin.requires_grad_(False)             # what PiDDecoderLoader._load_model does
x = torch.ones(1, 8, device="cuda")
with torch.no_grad(), torch.autocast("cuda", torch.bfloat16):
    a = lin(x).clone()
    lin.weight.mul_(1000)             # mutate the fp32 weight inside the autocast region
    b = lin(x)
print(torch.allclose(a, b))           # True = cached (stale copy), False = not cached

requires_grad_(False) prints False - no cache. Flip it to True and it prints True. Same thing at PiD level, where a 256px decode does negligible activation work so its peak is the weight cache:

256px decode peak    requires_grad=True    2707 MiB
                     requires_grad=False    109 MiB   <- production

requires_grad=False also survives partial loading: cached_model_with_partial_load.py:306 uses load_state_dict(..., assign=True), which propagates param.requires_grad.

Corrected measurements

Real PiD_res2k_sr4x_official_flux_distill_4step checkpoint loaded through the loader's own helpers, requires_grad_(False) applied, 4 steps, B=1, RTX 4090 / torch 2.7.1+cu128. RoPE caches cleared before every run - PiTBlock._fetch_pos memoizes per (Hs, Ws), so whichever decode runs first at a resolution pays that allocation inside its own peak and the numbers otherwise depend on measurement order. Values below are the cold peak, which is what a reservation has to cover. All MiB, floor = device_working_mem_gb = 3072.

            real peak      |        as-is (with term)            |            term removed
 output       off       on |  est off  est on  resv off  resv on |  est off  est on  resv off  resv on
   1024      1033      572 |     3598    3302      3598     3302 |     1000     704      3072     3072
   2048      4103     1795 |     6598    4742      6598     4742 |     4000    2144      4000     3072
   2560      6362     2817 |     8848    5822      8848     5822 |     6250    3224      6250     3224
   3072      9134     4037 |    11598    7142     11598     7142 |     9000    4544      9000     4544
   4096       OOM     7358 |    18598   10502     18598    10502 |    16000    7904     16000     7904

margin of the effective reservation over the real peak (negative = under-reserved):
 output    as-is off    as-is on   removed off   removed on
   1024        +2565       +2730         +2039        +2500
   2560        +2486       +3006          -112         +407
   3072        +2465       +3106          -134         +507
   4096    n/a (OOM)       +3144     n/a (OOM)         +546
   2048        +2495       +2947          -103        +1277

(4096px unoptimized was not measured - ~16 GiB of activations on top of the 5197 MiB resident net does not fit on a 24 GiB card. That part of my earlier review stands: at 4096px the flag is what makes the decode run at all.)

So the real shortfall I should have reported on 2026-08-08 was 103-134 MiB in the unoptimized path, not 1429-3662 MiB, and the term that was added to cover it over-reserves by ~2.6 GiB on every decode - including with the flag off, which makes it a regression against main for users who never enable the setting.

What I am asking for - three changes, and this is my final position on it

1. Remove the weight-cache term.

  • invokeai/backend/pid/decode.py:185-193, and the + autocast_weight_cache_bytes at :194 and :202-205
  • the model / device keyword-only parameters at decode.py:161-163, the docstring sentence at :169-171, and the comment fragments at :128 and :133
  • model=pid_info.model / device=pid_info.compute_device at flux2_pid_decode.py:213, flux_pid_decode.py:139, pid_upscale.py:183, qwen_image_pid_decode.py:205, sd3_pid_decode.py:136, sdxl_pid_decode.py:181, z_image_pid_decode.py:182
  • the sentence "CUDA working-memory reservations also include temporary bf16 autocast copies of the float32 PiD weights; those model-sized copies are not included in the activation figures." in docs/src/content/docs/configuration/low-vram-mode.mdx:160 and docs/src/content/docs/features/pid-decode.mdx:78
  • tests/backend/pid/test_pid_decode.py:176 (test_working_memory_estimate_includes_cuda_autocast_weight_cache) asserts the term is charged, using torch.nn.Linear(4, 4) with its default requires_grad=True; tests/app/invocations/test_pid_memory_optimization_wiring.py:70-79 hard-requires model= and device= on every call site. Both need to be inverted or dropped.

To expose this, a test that applies requires_grad_(False) to the model it passes - the state the loader leaves the net in - and asserts the estimate equals the activation-only estimate.

2. Raise _PID_DECODE_WORKING_MEMORY_SCALING_CONSTANT from 250 to ~260. This is separate from the term and pre-exists on main; today the phantom term hides it. Measured requirement against U = out_h * out_w * 4: 256.4 at 2048px, 254.5 at 2560px, 253.7 at 3072px. 260 covers all of them with 1.4-2.5% margin. Below 2048px the 3 GiB floor covers the gap, which is why it has not surfaced.

3. Keep the chunked constants exactly as they are. _PID_DECODE_CHUNKED_SCALING_CONSTANT = 120 and _PID_DECODE_CHUNKED_FIXED_BYTES = 224 MiB from ec48ddf5c7 are correct and needed - they cover every resolution up to 4096px, tightest at 2560px (+407 MiB, 14%) and 4096px (+546 MiB, 7%). That half of that commit was the right fix; only the weight term needs to go.

Verified against the real checkpoint - please do not change

  • Savings figures. ~3.7 GB -> ~1.5 GB and ~0.9 GB -> ~0.5 GB reproduce. The calibration comment at decode.py:141-143 (509 / 1533 MiB) is accurate.
  • Quality claim. Three real images through the full path (real Gemma-2 caption embeddings, real FLUX VAE latent): PSNR 56.0 / 56.0 / 50.3 dB at 2048px, 52.7 dB at 1024px, against the documented ~43 dB. Worst image: 99.97% of subpixels within 8 levels, 0.0001% above 64. "Visually indistinguishable but not bit-exact" is accurate and conservative.
  • Speed claim. 3.25-4.17 s off vs 3.24-4.26 s on at 2048px; isolated chunking cost +4.1%. "Roughly unchanged" holds.
  • Chunking correctness. Chunked and unchunked PiTBlock are equivalent modulo reassociation, including chunk boundaries that straddle images at B=2. Batch scaling of the estimate holds at B=2.
  • Full PiD suite passes: 100 tests. Note that test_chunking_stays_within_the_documented_tolerance_on_cuda_bf16 is the only test covering the shipped CUDA path and will skip on GPU-less CI.

JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 12, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 13, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto pushed a commit to JPPhoto/InvokeAI that referenced this pull request Aug 13, 2026
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
JPPhoto and others added 13 commits August 12, 2026 21:38
…at it costs

Addresses every point from the review of invoke-ai#9460.

The setting freed activation memory that the cache then withheld anyway.
`estimate_pid_decode_working_memory` was not flag-aware, so a decode that peaks at 1.5 GiB reserved
the unoptimized 3.9 GiB; the cache takes max(working_mem_bytes, device_working_mem_gb) and subtracts
that from the weight budget, so the saving never became weight residency - it only avoided a hard
OOM, on precisely the low-VRAM machines this feature exists for. The estimate now takes the flag,
and each node reads the setting once and feeds both the estimate and the decode from it, so the two
cannot drift apart.

Calibrated against measured peaks (RTX 4090, fp32 PidNet, bf16 autocast, 4 steps, B=1):

    1024px   509 MiB      1536px   934 MiB      2048px  1533 MiB

which is 85.3 * U + 167 MiB, not a pure multiple of the output area - chunking bounds the per-block
activations to a fixed working set, so a single scaling constant would under-reserve at small sizes
or over-reserve at large ones. The constants carry ~15% headroom. Below the chunk size the pixel
blocks run unchunked, so the estimate clamps to the unoptimized one rather than charging for a
working set that is never allocated.

The documented cost was the wrong cost. "at the cost of slower decoding" is not observable - 2.78 s
either way at 2048px, median of 3 with warmup - while the cost that is real went unmentioned: the
option changes the decoded image. Both parts are non-bit-exact and the few-step sampler amplifies
them (~43 dB PSNR end-to-end, visually indistinguishable, not reproducible against an unoptimized
decode). The setting description and both docs pages now state that, with the measured VRAM numbers.

The equivalence test could not fail. It asserted `assert_close` at pixel_hidden_size=4 / BL=8 /
chunk=3 on the CPU, while the shipped path is CUDA under bf16 autocast with BL in the thousands.
Measured at production dimensions: CPU fp32 is bit-identical (including B=2, where chunks straddle
images), CUDA fp32 differs by 9.5e-07, CUDA bf16 by 1.57e-02 - systematic, not noise, both paths
being internally deterministic. So chunking is exact as *mathematics*; the divergence is cuBLAS
picking different kernels for 1024-row slices. `test_pid_chunked_equivalence.py` now pins both
halves: exactness on the CPU, and an absolute tolerance contract on CUDA with 3x headroom (relative
tolerances are meaningless here - activations cross zero, so max|rel| reaches 1e3 on elements whose
absolute error is one bf16 ULP).

Two review points did not survive measurement, and are documented rather than "fixed":

- The fp32 `_velocity_to_x0` branch is kept. Fusing the multiply-subtract in fp64 is bit-identical to
  the default expression but frees nothing (288 MiB either way at 2048px), so the 192 MiB is bought
  entirely with precision - max|diff| 4.8e-07 per call, 8.6% of the flag's total saving. That is a
  fair trade for someone who opted into trading quality for VRAM, but it is now written down, in the
  function, in the setting description and in the docs.
- The doubled `adaLN_modulation` per chunk stays. Global attention sits between the two halves, so
  reusing the slices means holding them for every chunk - the full-resolution tensor the path exists
  to avoid (536 MiB in bf16 at 2048px). Projecting only the needed slices is no better: they are
  interleaved per pixel position, so selecting them gathers rows of a 1536x24576 weight, ~50 MiB per
  call. The recompute is ~9.9 TFLOP per 2048px decode, about 4% wall clock - and the honest source of
  the "slower decoding" the setting advertises.

Observability: a decode with the flag on now logs the resolution, the patch-token count and whether
chunking actually engaged. The setting is server-wide and never enters image metadata, so this is the
only record that a given decode ran optimized, and the only feedback that a yaml-only,
restart-required knob took effect at all.

Tests: AST sweeps assert that every module building a `PiDDecodeConfig` forwards the flag, estimates
working memory for the same mode, and reads the setting exactly once - so an eighth PiD node is
covered the day it lands. Plus estimate behaviour (shrinks when enabled, keeps the fixed term, never
exceeds the unoptimized estimate, still returns 0 for unsupported backbones), batch > 1 across chunk
boundaries, and the two paths the flag must not reach: the discriminator feature extraction returns
before the pixel blocks, and context parallelism is unreachable in this codebase (its only caller
lives in a vendored class InvokeAI never instantiates) - both pinned so a refactor cannot quietly put
them under the flag.

All new tests mutation-verified: breaking the chunked assembly, making the estimate ignore the flag,
dropping the fixed term, and omitting the flag from a node's decode or estimate each fail at least
one test.

tests/app + tests/backend/pid: 2202 passed. The 9 failures are the pre-existing network-dependent
ones in test_model_install / test_load_api / test_download_queue.
…cuous

CI caught this on macos-default py3.11; every other job in the matrix was cancelled by fail-fast.
Test-only change, no production code touched.

Two separate mistakes, both mine:

1. The CPU comparison asserted `torch.equal`. That held on x86-64 with MKL and failed on
   macOS/Accelerate. Splitting a GEMM along its row dimension can select a micro-kernel with
   different K-blocking, so bit-exactness there is a property of the BLAS, not of the chunking.
   Only reassociation-closeness is portable.

2. Worse, and only found while investigating the first: the `batch_size=1` parametrization never
   entered the chunked path at all. The dispatch guard is `BL > chunk_size`, and 512px with B=1 puts
   BL at exactly 1024 - so it compared the unchunked path against itself and passed for the wrong
   reason. Verified by spying on `_forward_chunked`: zero calls.

Both cases now demonstrably chunk - 768px/B=1 (BL 2304, boundaries inside one image) and 512px/B=2
(BL 2048, boundaries straddling images) - and a context manager fails the test if
`_forward_chunked` is not entered, so the comparison cannot silently empty out again.

Bit-equality is replaced by a signal-relative bound, calibrated rather than guessed. At these
dimensions the signal is ~5.7, so one fp32 ULP is ~6.8e-07:

    correct code, x86-64/MKL             max|diff| = 0
    attention contribution off by 1e-6   max|diff| = 7.2e-07  (1.3e-07 relative, sub-ULP)
    attention contribution off by 1e-4   max|diff| = 1.0e-05  (1.9e-06 relative, ~15 ULP)

1e-5 relative is ~84 ULP: above any BLAS reassociation, four orders of magnitude below a structural
break. The docstring states what that gives up - a uniform scaling error below ~2e-06 relative is
indistinguishable from legitimate reassociation and no portable test can claim it - and what it
still guards, which is the bug class that matters.

Mutation-verified against realistic breakage: an off-by-one on the last chunk, a wrong `s_cond`
slice, and skipping the chunked path each fail 5 of the 7 tests. 98 passed locally, ruff clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14 Nice-to-Have 6.14.0 backend PRs that change backend files docs PRs that change docs frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-tests PRs that change python tests services PRs that change app services

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

2 participants