Repository navigation
feat(working-memory): reserve FLUX.2 denoise working memory from measured lines - #9927
Conversation
Runs each installed model family through a launched scratch server and compares PSNR, time and driver-level VRAM against accepted references. Graphs come from the frontend builders via an offline graph export; the generate submission plan is shared with the workbench.
VAE estimators return a WorkingMemoryEstimate built from fitted and analytic terms; model_on_device accepts it as working_memory=. The AutoencoderKL coefficients and their measured points move to backend/working_memory/data, checked by a data test; reservations are unchanged.
A probe records each estimated model_on_device window's device-level demand inside a calibration scope; results go to a per-device-class store under databases/working_memory. Admin endpoints start, inspect, reset and export runs; the reserved queue origin is refused for ordinary enqueues.
VAE estimators price a measured slope and base per conv backend, allocator and dtype instead of scaling one bf16 constant by element size; CogView4 gets its own lines. SD/SDXL nodes and their estimate share one run-dtype rule; on ROCm Windows an SDXL fp32 1024² decode reserves 6.2 instead of 14.6 GB and the UNet stays resident.
A lower second reading at a measured size no longer tilts a fitted line, and points that include cast weight growth no longer fit one; two shipped lines move by under 1 %.
…n fact Environment ids now include windows, linux or other, so calibrations from different operating systems are kept apart; the OS build string moves to soft fact os_build.
… runs start --calibrate submits each case through the calibration endpoint and saves the store export per server lifetime; scripts/working_memory/build_shipped.py turns exports into probe points and refits. Probe points bound estimates from below only, and the builder refuses data that the shared bound rule rejects.
# Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx
…anager Adds a working-memory section to the model detail: plan, calibrate, stop and reset per GPU, with the measured, built-in and calibrated estimate per size. User fits from finished calibration attempts price VAE decodes within their measured range (+10 % + 256 MiB on the fitted part) and never below built-in values above it. New plan and summary routes; summary reports lines shared with other models and returns 404 for unknown models.
…calibration Judges allocator fragmentation per calibration attempt with one rule shared with the shipped-data builder, and moves other models off the GPU before a measured calibration lock. User fits use the shipped 5 % margin; the plan sizes cases from the capacity the cache will grant, including memory the driver keeps after earlier runs. The table marks uncounted sizes with their reason and explains each value column.
# Conflicts: # invokeai/backend/util/vae_working_memory.py # tests/app/invocations/test_flux2_working_memory.py
# Conflicts: # invokeai/backend/util/vae_working_memory.py
Main added useNsfwChecker and useWatermark to the generation project settings; exported calibration and E2E graphs run without both, as the defaults do.
…ser-calibration # Conflicts: # invokeai/frontend/webv2/performance/browser-baseline.json # invokeai/frontend/webv2/scripts/mock-backend-fixtures.test.mjs # invokeai/frontend/webv2/scripts/mock-backend.mjs
…e baseline Raises otherRawBytes to the measured 215114 B on every route; the 3791 B growth are the Model Manager working-memory strings.
…f the model page The section starts collapsed and shows the GPU with the calibration state, or the run progress, in its header; the plan loads on first open. The mock prices sizes above the measured range with the larger of the user line and built-in, as the server does.
Main added useNsfwChecker and useWatermark to the generation project settings; exported calibration and E2E graphs run without both, as the defaults do.
…sured lines Prices the Z-Image denoise through an EstimatorSpec whose per-token lines are fitted from fresh-process measurements on CUDA and ROCm, with an fp8 cast term and the legacy constants as the floor where nothing was measured. Fitted lines can now run over tokens; the builder takes measured point files, and calibration scripts share the server runtime and the probe's measurement window.
A fit-grid forward whose estimator line does not admit its features ships with in_fit false, beside the checks.
…ibration # Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx # invokeai/app/util/torch_cuda_allocator.py # invokeai/backend/working_memory/coefficients.py # invokeai/backend/working_memory/fit.py # invokeai/backend/working_memory/platform.py # tests/backend/model_manager/load/model_cache/test_model_cache_unlock_restores_casts.py # tests/backend/working_memory/test_fit.py # tests/backend/working_memory/test_shipped_coefficients.py
The runner checks the run is still running, spawns the server and records it in the heartbeat in one run-lock hold, so stop either sees the server or no server starts. A server whose identity cannot be recorded is killed before the lock is released. Points of estimators keyed by no platform trait name no platform in build_shipped.
…er model identity Calibrate requests carry graph_models, the hash, base, type, format and variant of every model the graph names as the client compiled it; a difference from the current records, or from a keyed repeat's run, is refused with 409 model_changed. The model manager and the family E2E tool send the identities of the records they compiled over.
# Conflicts: # invokeai/backend/working_memory/fit.py # scripts/working_memory/build_shipped.py
# Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx # invokeai/backend/working_memory/bounds.py # invokeai/backend/working_memory/fit.py # scripts/working_memory/build_shipped.py # tests/backend/working_memory/test_fit.py # tests/backend/working_memory/test_shipped_coefficients.py
# Conflicts: # invokeai/backend/working_memory/bounds.py
…ibration # Conflicts: # docs/src/content/docs/development/Guides/family-e2e.mdx # scripts/family_e2e/__main__.py # scripts/family_e2e/client.py # scripts/family_e2e/records.py # scripts/family_e2e/references.py # scripts/family_e2e/report.py # scripts/family_e2e/runner.py # scripts/family_e2e/server.py # tests/family_e2e/fake_invokeai_server.py # tests/family_e2e/test_cli.py # tests/family_e2e/test_references.py # tests/family_e2e/test_report.py # tests/family_e2e/test_runner.py
…king-memory-user-calibration
…king-memory-denoise
…king-memory-flux2-denoise
lstein
left a comment
There was a problem hiding this comment.
Review of this PR's own diff (475dd91a95..1f55613faa, on top of #9919). I found no crash and no wrong-input wiring, and the main claims hold up (see the end). One finding needs a measurement or a term before merge; the rest are small.
Needs a fix or a justification
1. Dropping the 1 GiB base uncovers regimes that no point measures.
The CUDA native entry has b_denoise_bf16_h* = 0 at every width (denoise.json:116-121). The old formula added a flat 1 GiB. All 144 points are a fully resident 2-block model without a mask, so two real cases have less headroom than before, and nothing measures them:
- Partially loaded [dev] bf16. Weights streamed per forward under partial loading are not priced;
probe.py:39-40and the model-manager docs say so. [dev]'s largest single-block linear (to_qkv_mlp_proj, 6144 × 55296) is about 680 MB in bf16, and it is alive during its own matmul alongside the activations. At 1024² with one 1024² reference (8704 tokens) on CUDA native, the reservation drops from 6.35 GiB to 4.92 GiB. The line's margin over the envelope at that size is only about 250–600 MB. The real-model checks cover resident fp8 Klein 4B/9B; there is no [dev] window, and [dev] is where the gap is largest. - Regional prompting. No point has
has_attention_mask=true, so a masked forward prices as the fused line plus the bias, with no base. On CUDA the mask goes to the memory-efficient kernel, whose footprint with a bias is unmeasured. It may also pad the bias to an 8-aligned stride, making a second bias-sized copy; I have not verified this on this torch build. Example: Klein 9B at 1440² with two 512-token regional captions, S = 9124. That reserves 3.71 GB plus the bias, against 5.09 GB plus the bias before.
The cache's device_working_mem_gb floor (model_cache.py:2242) covers small sizes, but not these.
Ask: measure a partially loaded [dev] on CUDA (and ideally one masked forward), or price the largest non-resident layer as a term.
Smaller items
- Fun-ControlNet lock under calibration (
flux2_denoise.py:~553). The branch locks with plainreservation_bytes, while the transformer's calibration lock keepscalibration_reservation(R)free. Loading the branch can therefore use up the calibration headroom, and the window then spills or OOMs instead of measuring. No calibration graph includes a control branch today, so this cannot happen yet. Still, the comment "Locked with the same working memory" is wrong inside a calibration scope. Passingprobe.calibration_reservation(...)when a scope is open would fix it. - Linux CUDA uses the Windows-measured lines without the unmeasured floor:
MEASUREDhas("cudnn", …, None). If that is intentional, say so in the module docstring. - The dry-run text in
scripts/calibrate_flux2_denoise_working_memory.pyis wrong (:37,:500). It says it prices "with the shipped values of cuDNN under the native allocator". A CPU is an unmeasured platform, sounmeasured_floorraises it to the legacy constants:--dry-run --widths 4096shows 2.89 GiB at 4608 tokens, not the fit's ~1.75 GiB. measured_bytes_and_methodcan record a point low (measurement.py:650). If the cold window has a driver reading and the warm one does not, the function falls back to torch counters only and drops the cold window's driver figure. On HIP that figure is ~245–300 MiB above reserved for a first forward. Scripts only.
Test gaps (a plausible regression passes)
- Script model geometry. No test ties the script's
_transformer(calibrate_flux2_denoise_working_memory.py:283-292) to the loader's construction (model_loaders/flux.py). Mutatingnum_attention_heads=hidden_size // HEAD_DIMto24measures every width at Klein 4B's inner width and fits it under 4096/6144, and no test fails. Comparinginner_dim == hidden_size, or comparing against the loader's config, would catch this. - Node wiring. No node test passes a regional mask or a negative caption. Both of these mutations survive:
regional_attn_mask=... if ref_image_seq_len == 0 else Nonechanged toNone;text_seq_len=max(txt.shape[1], neg_txt...)changed totxt.shape[1].
fresh_points.measure_in_this_process. The test does not pin the order ofapply_server_runtimebeforemeasure, which is the contract it describes.- Duplicated tests.
TestFlux2DenoiseWorkingMemoryEstimateandTestFlux2DenoiseBatchIsBudgetedintest_flux2_working_memory.pylargely duplicate the new file's coverage. Their docstrings still cite the retired measurements. test_each_widths_slope_covers_the_rx_9070_xts_closelyevaluates on thelegacyentry.TestLegacyReservationalready freezes those constants.
Checked and holding
- The node passes the same features as before:
- tokens = image + reference + max of the CFG pair's captions (the passes run sequentially);
- batch from
x.shape[0], with references repeated per sample; - the mask dropped when references are present.
- The control, LoRA and dequant terms are unchanged; the fp8 cast term only adds.
- The new
ValueErroris unreachable: the node always runs in bf16, and every variant maps to 3072, 4096 or 6144. - The legacy constants are 330300, 440401 and 660601 bytes per token plus a 1 GiB base, matching the old arithmetic. Every unmeasured platform resolves to these values or above.
- The non-legacy FLUX.2 entries' coefficients are reproduced exactly from their points. Every point is covered at ≥1.05×; above 1.3× there are only the two listed exceptions (1.417 and 1.391).
- Refitting on points ≤9216 tokens keeps every long point below its line, which supports the extrapolation.
- Z-Image and VAE data and reservations are identical to the base. The
off_line_reasonsrefactor keeps Z-Image's labels and fit membership. user_calibrated=Falseis honoured inuser_fit,fit_user_linesand the calibration fits service.- The Z-Image script refactor onto
fresh_pointspreserves the child protocol, the provenance string, the cold-OOM rule and the CLI guards.
Not checked: GPU behaviour. Finding 1 rests on layer sizes and the shipped data, not on an observed OOM.
…-denoise # Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx # docs/src/content/docs/development/Guides/family-e2e.mdx # invokeai/app/services/working_memory_calibration/working_memory_calibration_common.py # invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py # invokeai/app/services/working_memory_calibration/working_memory_calibration_fits.py # invokeai/backend/working_memory/bounds.py # invokeai/backend/working_memory/fit.py # invokeai/backend/working_memory/fragmentation.py # invokeai/backend/working_memory/spec.py # invokeai/backend/working_memory/user_fit.py # invokeai/frontend/api/openapi.json # invokeai/frontend/api/schema.ts # scripts/working_memory/build_shipped.py # tests/app/services/working_memory_calibration/test_working_memory_calibration_fragmentation.py # tests/app/services/working_memory_calibration/test_working_memory_calibration_user_fits.py # tests/backend/working_memory/test_fit.py # tests/backend/working_memory/test_shipped_coefficients.py # tests/backend/working_memory/test_user_fit.py # tests/test_build_shipped_working_memory.py
… transients Price the largest stored layer, which streams per forward under partial loading, and the padded bias copy that masked attention makes; bump the FLUX.2 denoise spec to version 2. Fun-ControlNet and model loads share one calibration-reservation rule; a cold window's driver reading survives a warm window without one. Tie the calibration script's geometry to the loader and cover the regional mask, negative caption and control-branch calibration paths.
…nkuchensack/InvokeAI into feat/working-memory-flux2-denoise
…-flux2-denoise # Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx # invokeai/app/invocations/z_image/z_image_denoise.py # invokeai/backend/util/vae_working_memory.py # invokeai/backend/working_memory/data/denoise.json # invokeai/backend/working_memory/fit.py # invokeai/backend/working_memory/measurement.py # invokeai/backend/working_memory/spec.py # invokeai/backend/z_image/denoise_working_memory.py # scripts/calibrate_z_image_working_memory.py # tests/app/invocations/test_z_image_denoise_working_memory.py # tests/backend/working_memory/test_measurement.py # tests/backend/working_memory/test_shipped_coefficients.py # tests/test_build_shipped_working_memory.py # tests/test_calibrate_z_image_working_memory.py
|
Thanks — finding 1 was right. Measured on a 4090 (CUDA native), FLUX.2 [dev] bf16 partially loaded (15 of 64 GB resident) needed 5509 / 5459 / 3305 MB at 8704 / 8612 / 4608 tokens against 5285 / 5229 / 2798 MB reserved (0.96× / 0.96× / 0.85×). The reservation now adds the largest layer's stored bytes (679 MB for [dev] bf16; fp8/GGUF are charged their stored size), which covers all three at 1.05–1.08×. A masked forward at S=9124 cost one extra bias-sized buffer (+158 MB); the padded copy is now priced. Spec bumped to v2. Items 2–5 and the test gaps are fixed; your mutations now fail the tests. |
lstein
left a comment
There was a problem hiding this comment.
Re-reviewed 07f31d88ec (head fdc0aaba79). All findings from the previous review are resolved:
- Finding 1: the largest stored layer is now reserved for partial loading, and the padded bias copy for masked attention. Your [dev] measurements sit at 1.05–1.08× with the new layer term.
- Shipped data: the only change in
denoise.jsonis the FLUX.2 version bump to 2; the points, coefficients and Z-Image data are identical. - Items 2–5 and the test gaps: fixed. I ran 1106 focused tests, all passing, and re-ran the mutations: the geometry, regional mask, negative caption, runtime order, streamed-layer and padded-bias terms, control-branch calibration reservation and mixed driver windows all fail the tests now.
Nit (non-blocking): changing the calibration script's weights_resident=True (scripts/calibrate_flux2_denoise_working_memory.py:~274) to False passes every test. If that happened, each newly measured point would carry streamed_layer_bytes, _line would put it off the line, and a calibration run would silently fit nothing. A test asserting that a script point's features have streamed_layer_bytes == 0 (or that it stays in_fit) would pin it.
…lux2-denoise Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
The FLUX.2 denoise reserved a hand-set 0.42 MiB per attended token, scaled linearly by transformer width, plus a fixed 1 GiB, on every platform. Fresh-process measurements on an RTX 4090 and an RX 9060 XT show this reserves too much. The denoise now runs through the working-memory estimator framework with one measured line per width.
cudaMallocAsyncEstimator
denoise.flux2prices a fitted line per width: Klein 4B 3072, Klein 9B 4096, [dev] 6144. The size is the attended tokens (image + reference + caption) × batch. Batch and reference images lie on the line.model_on_device(working_memory=…). User calibration stays VAE-only.Measurement
scripts/calibrate_flux2_denoise_working_memory.py. Its old denoise section is removed, and so is the old script's VAE section.scripts/working_memory/fresh_points.py: the point records, the server runtime, the child protocol and the driver. The cold-and-warm rule moves toinvokeai/backend/working_memory/measurement.py.fit.off_line_reasons).Related Issues / Discussions
QA Instructions
Measured points
working_memory/data/denoise.json:cudaMallocAsync;In-server check with real models, through the calibrate route. Real-model demand divided by the 2-block point at the same tokens:
A/B, family E2E tool, interleaved before → after → before, 1 warm-up + 3 measured, Klein 4B and Klein 9B at 1024²–2048²:
Tests
uv run --no-sync pytest -n 4over the working-memory, calibration-service, FLUX.2/Z-Image/VAE invocation, builder, calibration-script,fresh_pointsandrun_appenvironment suites: 3805 passed.Review
Resolved findings
Remaining limits
Compatibility / Rollout
scripts/calibrate_flux2_working_memory.pykeeps only its dispatch and SDPA checks.Checklist
What's Newcopy (if doing a release after this PR)Need help on this PR? Tag
@codesmith-botwith what you need.