diff --git a/README.md b/README.md index 5f7bede..39b74ee 100644 --- a/README.md +++ b/README.md @@ -159,7 +159,7 @@ The node remains local and is preserved without an input connection. Its **STRIN Marketplace searches for the distinct effective resource profiles in one plan run in parallel, with a maximum of eight concurrent requests. Successful results, including an empty result, are cached in the ComfyUI process for 60 minutes and shared across workflow submissions. Lease selection and acquisition remain ordered so later components can reuse capacity rented by earlier components. Restart ComfyUI to clear the in-memory cache; a rental availability race automatically forces one fresh marketplace search. -At queue time the planner raises **Any** or lower VRAM and RAM floors to whatever the workflow's models actually require, queries current on-demand offers, revalidates every enabled constraint locally, and ranks compatible offers by incremental cost — an existing managed lease gets credit for retention time already paid for. Numeric memory constraints use GiB, matching the existing scheduler and Vast offer-capacity calculations. Vast's marketed GPU names do not always equal their reported capacity exactly; for example, a reported 95.x GiB card does not satisfy a literal 96 GiB floor. The default idle retention is 24 hours, after which an in-instance watchdog destroys the idle lease. +At queue time the planner raises **Any** or lower VRAM and RAM floors to whatever the workflow's models actually require, queries current on-demand offers, and revalidates every enabled constraint locally. Cross-provider selection compares Vast's effective hourly compute price over the predicted execution time; configured idle retention is tracked as a separate billing estimate and does not make Vast appear more expensive than Modal or SSH during placement. Numeric memory constraints use GiB, matching the existing scheduler and Vast offer-capacity calculations. Vast's marketed GPU names do not always equal their reported capacity exactly; for example, a reported 95.x GiB card does not satisfy a literal 96 GiB floor. The default idle retention is 24 hours, after which an in-instance watchdog destroys the idle lease. Set credentials and the worker image in the ComfyUI process environment. Neither value is written to workflows, the lease registry, API responses, or logs: diff --git a/docs/LEARNINGS.md b/docs/LEARNINGS.md index fd9aa11..48131ef 100644 --- a/docs/LEARNINGS.md +++ b/docs/LEARNINGS.md @@ -506,3 +506,4 @@ - Vast marketplace selection works best when workflow-authored preferences are distinguishable from runtime requirements. Represent unconstrained selectors as `None`, omit them from the API payload, and then raise only VRAM/RAM to the planner's inferred component floors. Literal zero filters are not the same contract as an explicit `Any`, and a mandatory default such as 100 TFLOPS can eliminate otherwise suitable H200 capacity because marketplace compute scores are not comparable across GPU architectures. - Vast reports memory as an integer offer field whose displayed capacity can differ from marketed GPU names. Keep the workflow field explicitly GiB-based and do not silently treat a marketed “96 GB” label as proof that the offer meets a literal 96 GiB threshold. - A disconnected queue-time configuration node can still expose placement results without becoming part of the remote graph. Request ComfyUI's hidden rewritten prompt, stamp only credential-free lease metadata into proxy payloads, filter by the configuration node's hidden unique ID, and fingerprint the generated Markdown so cached local output changes with the actual selected GPU type. +- Provider selection must compare equivalent compute rates. Price a Vast candidate from its marketplace hourly rate over predicted execution time, and retain the full execution-plus-idle-retention amount only as a separate lifecycle billing estimate; folding 24 hours of optional retention into placement cost makes otherwise-cheaper Vast hardware lose every automatic comparison. diff --git a/tests/test_vast_service.py b/tests/test_vast_service.py index b3cc053..218bb7b 100644 --- a/tests/test_vast_service.py +++ b/tests/test_vast_service.py @@ -29,7 +29,7 @@ async def _running_app(app: web.Application) -> AsyncIterator[str]: await runner.cleanup() -def test_quote_raises_memory_floors_and_prices_retention( +def test_quote_raises_memory_floors_and_compares_effective_hourly_price( vast_api_module: Any, vast_models_module: Any, vast_runtime_module: Any, @@ -37,7 +37,7 @@ def test_quote_raises_memory_floors_and_prices_retention( vast_simulator_module: Any, tmp_path: Path, ) -> None: - """A quote should select the cheapest offer after inferred memory and cooldown.""" + """Scheduling should exclude retention cost while still exposing its estimate.""" async def scenario() -> None: """Exercise the production client and service against the simulator.""" @@ -80,10 +80,12 @@ def test_quote_raises_memory_floors_and_prices_retention( assert quote.offer.offer_id == 1002 assert quote.profile.minimum_gpu_ram_mb == 40 * 1024 assert quote.profile.minimum_cpu_ram_mb == 96 * 1024 - assert quote.predicted_incremental_cost_usd == ( + assert quote.predicted_incremental_cost_usd == 120 / 3600 * 0.74 + assert quote.predicted_retention_cost_usd == ( (24 * 3600 + 120) / 3600 * 0.74 ) assert scheduling_state.provider.value == "vast" + assert scheduling_state.cost_usd_per_second == 0.74 / 3600 assert scheduling_state.capabilities.maximum_vram_bytes == 48 * 1024**3 asyncio.run(scenario()) diff --git a/vast_service.py b/vast_service.py index 1498e58..d36229c 100644 --- a/vast_service.py +++ b/vast_service.py @@ -65,7 +65,7 @@ VAST_OFFER_PREFETCH_CONCURRENCY = 8 @dataclass(frozen=True) class VastProfileQuote: - """Describe the least incremental-cost profile for one component.""" + """Describe the least effective-hourly-price profile for one component.""" profile: VastResourceProfile offer: VastOffer | None @@ -83,6 +83,11 @@ class VastProfileQuote: raise RuntimeError("Vast quote has neither an offer nor a lease.") return self.offer.hourly_cost_usd + @property + def predicted_retention_cost_usd(self) -> float: + """Return the incremental bill estimate including configured retention.""" + return self.retention_seconds_charged / 3600.0 * self.hourly_cost_usd + @dataclass(frozen=True) class VastSearchRequirements: @@ -202,7 +207,6 @@ class VastService: return min( candidates, key=lambda quote: ( - quote.predicted_incremental_cost_usd, quote.hourly_cost_usd, quote.profile.profile_id, ), @@ -303,16 +307,16 @@ class VastService: profile: VastResourceProfile, predicted_execution_seconds: float, ) -> VastProfileQuote: - """Quote one profile, recognizing already-paid idle lease coverage.""" + """Quote compute at its hourly rate while tracking retention separately.""" existing = self._existing_lease(profile) if existing is not None: - incremental_cost = self.lease_manager.incremental_retention_cost_usd( + retention_cost = self.lease_manager.incremental_retention_cost_usd( profile, existing_lease=existing, predicted_execution_seconds=predicted_execution_seconds, ) charged_seconds = ( - incremental_cost / existing.hourly_cost_usd * 3600.0 + retention_cost / existing.hourly_cost_usd * 3600.0 if existing.hourly_cost_usd > 0 else 0.0 ) @@ -320,7 +324,11 @@ class VastService: profile=profile, offer=None, existing_lease=existing, - predicted_incremental_cost_usd=incremental_cost, + predicted_incremental_cost_usd=( + predicted_execution_seconds + / 3600.0 + * existing.hourly_cost_usd + ), retention_seconds_charged=charged_seconds, predicted_execution_seconds=predicted_execution_seconds, ) @@ -336,7 +344,7 @@ class VastService: offer=offer, existing_lease=None, predicted_incremental_cost_usd=( - charged_seconds / 3600.0 * offer.hourly_cost_usd + predicted_execution_seconds / 3600.0 * offer.hourly_cost_usd ), retention_seconds_charged=charged_seconds, predicted_execution_seconds=predicted_execution_seconds, @@ -378,10 +386,7 @@ class VastService: provider=ExecutionProvider.VAST, enabled=True, health=EnvironmentHealth.READY, - cost_usd_per_second=( - quote.predicted_incremental_cost_usd - / max(1.0, quote.predicted_execution_seconds) - ), + cost_usd_per_second=quote.hourly_cost_usd / 3600.0, capabilities=EnvironmentCapabilities( architecture="x86_64", operating_system="linux",