Pixal3D PR-3: decouple model-queue binding from resident mode + per-model routing allow-list #836

Closed
opened 2026-07-25 00:53:48 +00:00 by spikerj · 1 comment
Owner

Part of #833.

Problem A — model-queue consumption implies a lifetime GPU lease

ArtPipeStageConsumer.ResolveBindings welds "consume art.model.<dir>.tasks" to resident mode, which also implies a persistent --serve subprocess and a process-lifetime GPU lease (ResidentGpuLeaseHolder, released only on shutdown). For a WRAP backend there is nothing to keep warm, so a resident Pixal3D would pin 18 GB of the 4090's 24 GB permanently for zero benefit, starving every other tenant on the lane.

The alternative — a dedicated Stages=["modeling"] stack — is worse: stage queues are durable and model-agnostic, so it would round-robin-split modeling traffic with spikersoft-artpipe-modeling and receive TripoSG-selected jobs it can't run.

Fix: an ArtPipe:ModelQueue key that makes ResolveBindings bind the model task queue without entering resident mode. ResolveBindings is internal static and pure precisely so tests pin topology.

  • ArtPipeConfig: add ModelQueue; extend Validate() — mutually exclusive with both Stages and ResidentModel.
  • ArtPipeStageOrchestrator: extend the model guard so a ModelQueue instance rejects jobs for other models (today it's keyed on IsResidentMode only — without this a stray message would run an arbitrary model).
  • Executor selection is untouched: not resident ⇒ SubprocessArtPipeStageExecutor (fresh subprocess per job) + the per-job lease. That's the per-message semantics we want.

Problem B — model-queue routing is global, not per-model

Today it's one bool (ArtPipe:PublishToModelQueues) plus one stage→model map (ArtStudio:StageModelMap), and both publish sites route every model for that stage to a model queue. That breaks the moment Pixal3D is the only model with a dedicated consumer:

  • ArtStudio__StageModelMap__modeling=Pixal3D would send a triposg-selected job to art.model.triposg.tasksResolveResidentModelDir returns method.ModelDir whenever a method is selected.
  • PublishToModelQueues=true on spikersoft-artpipe-modeling would send the default TripoSG path there too — ResolveNextStageRoutingKey falls back to that stack's StageSettings["modeling"].ModelDir.

Neither queue has a consumer, so those jobs land in art.asset.unrouted.messages and fail — they do not fall back to the stage queue.

Fix: an allow-list of ModelDirs known to have live consumers, consulted at both publish sites (the API handler and the worker's next-stage hop).

Verification

  • ResolveBindings topology tests extended for ModelQueue
  • Routing tests covering both publish sites: Pixal3D → model queue, TripoSG → stage queue
  • dotnet build SpikerSoft.UnitTests.slnf clean
Part of #833. ## Problem A — model-queue consumption implies a lifetime GPU lease `ArtPipeStageConsumer.ResolveBindings` welds "consume `art.model.<dir>.tasks`" to resident mode, which also implies a persistent `--serve` subprocess and a **process-lifetime GPU lease** (`ResidentGpuLeaseHolder`, released only on shutdown). For a WRAP backend there is nothing to keep warm, so a resident Pixal3D would pin 18 GB of the 4090's 24 GB permanently for zero benefit, starving every other tenant on the lane. The alternative — a dedicated `Stages=["modeling"]` stack — is worse: stage queues are durable and model-agnostic, so it would round-robin-split modeling traffic with `spikersoft-artpipe-modeling` and receive TripoSG-selected jobs it can't run. **Fix:** an `ArtPipe:ModelQueue` key that makes `ResolveBindings` bind the model task queue *without* entering resident mode. `ResolveBindings` is `internal static` and pure precisely so tests pin topology. - `ArtPipeConfig`: add `ModelQueue`; extend `Validate()` — mutually exclusive with both `Stages` and `ResidentModel`. - `ArtPipeStageOrchestrator`: extend the model guard so a `ModelQueue` instance rejects jobs for other models (today it's keyed on `IsResidentMode` only — without this a stray message would run an arbitrary model). - Executor selection is untouched: not resident ⇒ `SubprocessArtPipeStageExecutor` (fresh subprocess per job) + the per-job lease. That's the per-message semantics we want. ## Problem B — model-queue routing is global, not per-model Today it's one bool (`ArtPipe:PublishToModelQueues`) plus one stage→model map (`ArtStudio:StageModelMap`), and both publish sites route *every* model for that stage to a model queue. That breaks the moment Pixal3D is the only model with a dedicated consumer: - `ArtStudio__StageModelMap__modeling=Pixal3D` would send a **triposg**-selected job to `art.model.triposg.tasks` — `ResolveResidentModelDir` returns `method.ModelDir` whenever a method is selected. - `PublishToModelQueues=true` on `spikersoft-artpipe-modeling` would send the **default** TripoSG path there too — `ResolveNextStageRoutingKey` falls back to that stack's `StageSettings["modeling"].ModelDir`. Neither queue has a consumer, so those jobs land in `art.asset.unrouted.messages` and fail — they do **not** fall back to the stage queue. **Fix:** an allow-list of ModelDirs known to have live consumers, consulted at **both** publish sites (the API handler and the worker's next-stage hop). ## Verification - [ ] `ResolveBindings` topology tests extended for `ModelQueue` - [ ] Routing tests covering both publish sites: Pixal3D → model queue, TripoSG → stage queue - [ ] `dotnet build SpikerSoft.UnitTests.slnf` clean
Author
Owner

Closing — both halves of this already exist on master, shipped under #357 (the per-model modeling lane). This ticket was written against a local checkout that was 184 commits behind origin/master.

Problem AArtPipeConfig.ModelQueue (ArtPipeConfig.cs:49) does exactly what was specced: binds art.model.<dir>.tasks without entering resident mode, so the instance keeps SubprocessArtPipeStageExecutor (fresh subprocess per job) and the per-job GPU lease. IsModelQueueMode / BoundModelQueueDir are the accessors; Validate() treats ResidentModel and ModelQueue as mutually exclusive modes and spells out the difference ("holding a resident lease; ModelQueue consumes that SAME per-model queue but runs a subprocess per job with a…").

Problem BModelQueueStages (ArtPipeConfig.cs:121) is the selective per-stage counterpart to the global PublishToModelQueues, with RoutesToModelQueue(stage) as the predicate. Its doc confirms both publish sites are covered: "Mirrors the API's ArtStudio:StageModelMap keys, which gate the SAME routing for first-stage / restart publishes." The prodstages monolith already sets ModelQueueStages=["modeling"].

There are also already five deployed per-model modeling stacks (triposg, shape, instantmesh, sf3d, plus hunyuan / sdxl / triposr / blender / safety), all following the ArtPipe__ModelQueue=<Dir> + ArtPipe__Stages= shape.

So Pixal3D needs no backend code change at all — it's a copy of the established spikersoft-artpipe-model-triposg stack. Folded into #838.

**Closing — both halves of this already exist on `master`, shipped under #357 (the per-model modeling lane).** This ticket was written against a local checkout that was 184 commits behind `origin/master`. **Problem A** — `ArtPipeConfig.ModelQueue` (`ArtPipeConfig.cs:49`) does exactly what was specced: binds `art.model.<dir>.tasks` *without* entering resident mode, so the instance keeps `SubprocessArtPipeStageExecutor` (fresh subprocess per job) and the per-job GPU lease. `IsModelQueueMode` / `BoundModelQueueDir` are the accessors; `Validate()` treats `ResidentModel` and `ModelQueue` as mutually exclusive modes and spells out the difference ("holding a resident lease; ModelQueue consumes that SAME per-model queue but runs a subprocess per job with a…"). **Problem B** — `ModelQueueStages` (`ArtPipeConfig.cs:121`) is the selective per-stage counterpart to the global `PublishToModelQueues`, with `RoutesToModelQueue(stage)` as the predicate. Its doc confirms both publish sites are covered: *"Mirrors the API's `ArtStudio:StageModelMap` keys, which gate the SAME routing for first-stage / restart publishes."* The prodstages monolith already sets `ModelQueueStages=["modeling"]`. There are also already five deployed per-model modeling stacks (`triposg`, `shape`, `instantmesh`, `sf3d`, plus `hunyuan` / `sdxl` / `triposr` / `blender` / `safety`), all following the `ArtPipe__ModelQueue=<Dir>` + `ArtPipe__Stages=` shape. So Pixal3D needs **no backend code change at all** — it's a copy of the established `spikersoft-artpipe-model-triposg` stack. Folded into #838.
Sign in to join this conversation.