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
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
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>.taskswithout 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.
**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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Part of #833.
Problem A — model-queue consumption implies a lifetime GPU lease
ArtPipeStageConsumer.ResolveBindingswelds "consumeart.model.<dir>.tasks" to resident mode, which also implies a persistent--servesubprocess 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 withspikersoft-artpipe-modelingand receive TripoSG-selected jobs it can't run.Fix: an
ArtPipe:ModelQueuekey that makesResolveBindingsbind the model task queue without entering resident mode.ResolveBindingsisinternal staticand pure precisely so tests pin topology.ArtPipeConfig: addModelQueue; extendValidate()— mutually exclusive with bothStagesandResidentModel.ArtPipeStageOrchestrator: extend the model guard so aModelQueueinstance rejects jobs for other models (today it's keyed onIsResidentModeonly — without this a stray message would run an arbitrary model).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=Pixal3Dwould send a triposg-selected job toart.model.triposg.tasks—ResolveResidentModelDirreturnsmethod.ModelDirwhenever a method is selected.PublishToModelQueues=trueonspikersoft-artpipe-modelingwould send the default TripoSG path there too —ResolveNextStageRoutingKeyfalls back to that stack'sStageSettings["modeling"].ModelDir.Neither queue has a consumer, so those jobs land in
art.asset.unrouted.messagesand 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
ResolveBindingstopology tests extended forModelQueuedotnet build SpikerSoft.UnitTests.slnfcleanClosing — 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 behindorigin/master.Problem A —
ArtPipeConfig.ModelQueue(ArtPipeConfig.cs:49) does exactly what was specced: bindsart.model.<dir>.taskswithout entering resident mode, so the instance keepsSubprocessArtPipeStageExecutor(fresh subprocess per job) and the per-job GPU lease.IsModelQueueMode/BoundModelQueueDirare the accessors;Validate()treatsResidentModelandModelQueueas 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 globalPublishToModelQueues, withRoutesToModelQueue(stage)as the predicate. Its doc confirms both publish sites are covered: "Mirrors the API'sArtStudio:StageModelMapkeys, which gate the SAME routing for first-stage / restart publishes." The prodstages monolith already setsModelQueueStages=["modeling"].There are also already five deployed per-model modeling stacks (
triposg,shape,instantmesh,sf3d, plushunyuan/sdxl/triposr/blender/safety), all following theArtPipe__ModelQueue=<Dir>+ArtPipe__Stages=shape.So Pixal3D needs no backend code change at all — it's a copy of the established
spikersoft-artpipe-model-triposgstack. Folded into #838.