Repository navigation
feat(working-memory): measure working memory in admin calibration runs - #9897
Pfannkuchensack wants to merge 16 commits into
Conversation
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.
…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.
# Conflicts: # invokeai/backend/util/vae_working_memory.py # tests/app/invocations/test_flux2_working_memory.py
# Conflicts: # docs/src/content/docs/development/Architecture/model-manager.mdx # invokeai/backend/model_manager/load/load_base.py
# Conflicts: # invokeai/app/services/invocation_services.py
… tests CPU wheels on macOS and Windows have no CUDA allocator binding, so the fixture that scripts a CUDA build scripts its backend too.
JPPhoto
left a comment
There was a problem hiding this comment.
@Pfannkuchensack Your review has arrived. Merge blockers:
-
Pending calibration work can fail during startup.
invocation_services.py:152starts the session processor before the calibration service. A resumed queue item can reachadmit()before the calibration service sets_invokerinworking_memory_calibration_default.py:178; the item then fails before measurement. Start the calibration service before session workers, or remove that ordering dependency. If skipped: a restart with pending calibration work can lose it. Likelihood: only when work is pending at startup. Recovery: rerun the failed request. Test: boot with a pending calibration item and verify it is not failed before service initialization. -
Failed or canceled nodes can produce valid measurements.
session_processor_default.py:314wrapsrun_node()in the probe scope, butrun_node()catches and swallows errors and cancellations atsession_processor_default.py:462. The scope therefore closes without an error, so its windows can remain valid. Propagate the node outcome to the scope. If skipped: failed executions can be treated as trustworthy calibration data. Likelihood: whenever a measured node fails or is canceled. Recovery: remove the affected measurements and rerun. Test: fail and cancel an invocation throughDefaultSessionRunnerand assert its windows are invalid. -
An idempotency conflict can delete a run another request is using. In
working_memory_calibration_default.py:314, the request that first creates a run deletes it on enqueue failure. A concurrent request with the same key but different payload can enqueue successfully first; the creator then deletes the shared run. Cancellation during the threaded enqueue has a similar risk. Protect creation and enqueue as one operation, and only discard a run when no enqueue committed. If skipped: queue items can lose their run and measurement provenance. Likelihood: concurrent or canceled same-key requests. Recovery: remove orphan queue items and rerun. Test: race different payloads under one key and cancel during enqueue. -
A key does not uniquely identify the calibration request. The run ID uses user and idempotency key, while the queue payload hash omits the model route. Reusing the same key and graph for model B after model A can return model A’s run as a success. Also, queue receipts expire after seven days, so a later retry can enqueue again into the persistent run. Persist and compare a request fingerprint that includes the model key and batch data. If skipped: results may have the wrong model provenance or duplicate samples. Likelihood: key reuse across models or after receipt expiry. Recovery: discard the incorrect run and retry with a new key. Test: reuse a key across model paths and after receipt expiry.
-
Partial batch admission is reported as success.
working_memory_calibration_default.py:316rejects only when zero items enqueue. The queue can admit only the available capacity, so a multi-item request can complete with a partial sample set. Require all items to enqueue or mark the run incomplete and clean up admitted items. If skipped: incomplete calibration data can appear complete. Likelihood: queue near capacity. Recovery: discard the partial run and rerun. Test: submit multiple batch values with capacity for only one.
Other findings/issues:
-
Counter failures can still yield valid windows.
counters.py:204swallows peak-reset and synchronization errors; callers continue measuring inprobe.py:402andprobe.py:519. Return failure or raise a probe-local error and mark the window invalid. If skipped: stale peaks or unsettled GPU work can contaminate trusted measurements. Likelihood: transient device/backend errors. Recovery: remove the affected point and rerun. Test: make reset or synchronization fail and assert the window is invalid. -
A lock can miss probe hooks during an arm race.
load_base.py:193snapshotsprobe.armedbefore locking. If it reads false, pauses, then the probe arms and opens a window, the lock proceeds without hooks and can contaminate that window. Coordinate the snapshot and hook registration, or ensure the hooks cover the interleaving. If skipped: concurrent model loading can be included in valid demand. Likelihood: narrow thread interleaving during calibration. Recovery: rerun the measurement. Test: use a barrier to force the interleaving. -
The generated API contract omits a new HTTP 400.
session_queue.py:272returns 400 for a reserved origin, but the route declaration and generatedopenapi.json:12884do not list it. Declare the response and regenerate the API artifact. If skipped: generated clients and docs miss a supported error response. Likelihood: client depends on generated contract. Recovery: regenerate and update clients as needed. Test: verify the route’s OpenAPI response includes 400.
…stworthy The session processor starts after every other service; windows of failed or canceled nodes, failed counter resets and locks racing a window are stored invalid. Keyed requests are identified by model, graph and batch data, are created once per run and admit all of their items or none; the reserved-origin 400 is declared.
…runs' into feat/working-memory-data
|
@JPPhoto Thanks for the thorough review — all eight points are addressed in 3ebdb7f (merged with main in 4fbf583).
Also: a keyless request whose items could not be recorded answers 201 with its queued items (they run) instead of an error; a run whose items were all canceled reads |
JPPhoto
left a comment
There was a problem hiding this comment.
New merge blockers:
- High - Shutdown can overlap old workers with teardown and restart.
invokeai/app/services/invoker.py:39signals workers, then immediately stops their services.invokeai/app/services/session_processor/session_processor_default.py:934does not wait for them to exit. A same-process restart can replace the device pool while an old worker still uses the GPU or stopped services. Fix/test: Wait for workers to exit before service teardown and pool replacement. Test stopping and reinitializing while a worker is held in a node.
Other findings/issues:
-
Medium - A detached locked model can escape probe validation.
invokeai/backend/model_manager/load/model_cache/model_cache.py:1892counts locks only on cached records, butinvokeai/backend/model_manager/load/model_cache/model_cache.py:2993can detach a still-locked record. A calibration window can miss the foreign lock and record contaminated memory as valid. Fix/test: Count active locks independently of cache membership. Open a window while a detached record remains locked. -
Medium -
reset_modelcan be undone by a pending measurement write.invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:560checks the run before writing. If another run measures the reset model, its later write can append the point afterreset_modelremoves it at line 754. Fix/test: Coordinate reset and writes. Block a write across reset and verify the point stays absent. -
Medium - Retry status loses lineage until admission.
invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:593reads live statuses withoutretried_from_item_id;derive_run_statuscan only link retries already observed by the runner. A canceled or pruned retry before admission leaves the original failure effective and may omit the retry from run item IDs, contrary to the documented retry contract. Fix/test: Preserve live retry lineage. Cancel and prune a retry before admission. -
Medium - Keyed recovery can fail if the model was removed. For a keyed run whose queue items were committed but not recorded,
_prepare_runatinvokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:403still looks up the model at line 416 before replaying the queue receipt. Removing the model before retry turns a recoverable request into a 404. Fix/test: Replay the matching receipt without requiring the model. Test after removing it. -
Medium - Resetting a run does not invalidate its idempotency key.
reset_runatinvokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:746removes the stored run but leaves queue receipts and items. Repeating the same keyed request can recreate the run and allow pending items to pass admission after reset; an expired receipt can also permit duplicate enqueue. Fix/test: Invalidate or tombstone the key on reset. Retry the same request with retained and expired receipts.
…ration writes behind resets Stop joins the workers within a shared 30 s deadline and refuses a restart while one still runs; the cache counts locks apart from its records. A model reset raises a per-model epoch the runner reads at node start, keyed runs leave a tombstone on reset, retries keep their lineage before admission, and a recorded receipt replays without the model.
|
@JPPhoto Thanks for the second pass. All six points are addressed in b012444. Each one has a test that fails without its fix. High: shutdown overlapping old workers
A detached locked record escaping the probe
Retry lineage before admission
Keyed recovery after the model was removed
Reset not invalidating the idempotency key
Store file: the two new fields ( Contract: no API shape or status changes. The OpenAPI descriptions are regenerated. The stack above (#9899, #9910, #9919, #9927, #9946) has these merged in. |
JPPhoto
left a comment
There was a problem hiding this comment.
Thanks; the six findings from the previous review appear fixed.
Merge blockers
-
Medium - Status can say calibration is available when the store cannot be written. In
invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:671-693, status checks whether the store file can be read, not whether a run can write it. A read-only store can reportavailable=true, then fail run creation with 503. Also,device_class_ids()can let a directoryPermissionErrorescape as 500 (working_memory_calibration_store.py:140-144). Cover read-only and unreadable store directories. -
Medium - A keyed retry can enqueue a second batch after an ambiguous failure. If queue admission succeeds but recording item IDs fails, the run remains with no IDs. If the key is acknowledged after that 503, its receipt expires, and the queue items are cleared or pruned before observation, a repeat falls through to enqueue at
working_memory_calibration_default.py:423-450. Preserve an admission marker or refuse to re-enqueue when admission is uncertain. Test that sequence.
Other issues
-
Low - A completed item can later appear canceled or partial. The queue records completion before the processor persists the terminal calibration observation (
session_processor_default.py:660-675,327-330). If the process stops in between and the completed row is later pruned, the savedin_progressobservation is treated as canceled (working_memory_calibration_default.py:151-152). Test completion followed by pruning before observation. -
Low - A conflicting key can return 404 instead of the documented 409. With a different request that names a missing model,
_prepare_run()looks up the model before queue admission detects the key conflict (working_memory_calibration_default.py:423-450). The endpoint documents 409 for key reuse with another request (invokeai/app/api/routers/working_memory.py:95-100). -
Low - Run summaries can omit a retry's source item.
derive_run_status()usesretried_from_item_idto determine effective status but does not add that source ID to the returneditem_ids(working_memory_calibration_default.py:144-166). If the source is absent from stored, observed, and live IDs, the summary violates its "every queue item" contract.
…in calibration runs Status reports a store it cannot write as unavailable, and a run is marked before its batch is enqueued so a repeat never enqueues it twice. A clean finish is recorded before the queue completes the item, a key conflict answers 409 before the model lookup, and run summaries list a retry's source.
|
@JPPhoto Thanks. All five points are addressed in 1370b23, and each has a test that fails without its fix. 1. Status available on a store it cannot write. Status now checks that the current device class's store can be written. It opens an OS-deleted temporary file in the store folder, or in the nearest existing folder above it, and checks that the lock file is writable. On Windows it also checks that the JSON file is not read-only. A store that fails this reports 2. A second batch after an ambiguous failure. A run now carries 3. A completed item later reading canceled. On a clean finish (no error, not canceled, and the queue row is neither failed nor canceled), the runner now records the item as completed before the queue completes it. The final observation still writes the queue's status. Tests:
5. 404 instead of 409. For a keyed request with no run, the reset check runs first (404), then the receipt check (409), and only then the model lookup. 7. Retry sources missing from Limits, documented:
Store file: one new field with a default; schema version 1. API: no shape or status changes. The OpenAPI descriptions are regenerated. The stack above has this merged in. |
JPPhoto
left a comment
There was a problem hiding this comment.
Findings:
-
Medium - Cross-process requests can return an empty success or enqueue twice. The per-run lock is process-local. If process A records the marked run and pauses before enqueue, process B can return
201withitem_ids=[]atinvokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:437-464; A may then enqueue or remove the run. If two prepared attempts overlap and one resumes after the first receipt is collected, both can enqueue, and the later item IDs can replace the earlier ones (:369-382,492-503). Use cross-process coordination or an explicit in-progress response. Add a two-process regression for both sequences. -
Medium - Final observation can overwrite a saved completion with canceled. The runner records completion before queue completion, but its
finallyblock callsobserve()afterward (invokeai/app/services/session_processor/session_processor_default.py:320-341). If the completed queue item is cleared or pruned in between,observe()records canceled over the saved completion (invokeai/app/services/working_memory_calibration/working_memory_calibration_default.py:619-633,650-655). Preserve an existing completed observation and test that sequence.
A keyed run's creation holds an OS file lock, striped over 256 files, from preparing the run to recording its items, behind a per-run in-process gate. The final observation keeps a recorded completion of an item the queue no longer has.
…waits for its workers Stop now joins the workers, so the wake-up test opens its gates before stopping and runs the stop case on a thread of its own.
|
@JPPhoto Thanks. Both points are addressed in 9f2345c, with tests that fail without the fixes. 1. Cross-process creation
2. Final observation overwriting a saved completion
Also changed: main's #9855 wake-up test. Merging main brought in #9855's
#9855's guarantee is unchanged. Making the workers share one The stack above has this merged in. |
Summary
Working-memory estimates can now be checked against what an operation really uses, on the machine that runs it. This PR only records measurements; no estimate changes.
invokeai/backend/working_memory/probe.py)model_on_device(working_memory=...)window records its demand next to its estimate.fdinfoon Linux, and the driver counter on MPS.invokeai/app/services/working_memory_calibration/)POST /api/v2/models/i/{key}/working_memory/calibrate.enqueue_batchrefuses for everyone.databases/working_memory/.Related Issues / Discussions
Builds on #9896.
QA Instructions
pytest -m slow tests/backend/working_memory/test_probe.pypassed on an RTX 4090 (CUDA 13.0) and an RX 9060 XT (ROCm 10, Windows): the window is valid and covers the reserved growth.cudaMallocAsync):flux_vae_encode: reserved 2200 MiB, measured 1840 MiB.flux_vae_decode: reserved 4400 MiB, measured 2912 MiB.openapi.json/schema.ts;pnpm lintandpnpm testininvokeai/frontend/apipass.Review
Resolved findings
finally, and an item that is gone counts as canceled.calibratewas not safe to retry after a lost response.os.replacefailed transiently while another process held the file.availableand the declared error codes now match what the routes do.Remaining limits
fdinfo, MPS and XPU readers follow vendor documentation, untested on hardware. They are markedverified=false.torch.cuda.empty_cache()calls and background offloads bypass the segment boundary. The torch peaks still bound the demand.Compatibility / Rollout
<db_dir>/working_memory/<device_class>.json.Checklist
What's Newcopy (if doing a release after this PR)Need help on this PR? Tag
@codesmith-botwith what you need.