diff --git a/examples/advanced/hello-world.ts b/examples/advanced/hello-world.ts index 118bb8734..3953aa794 100644 --- a/examples/advanced/hello-world.ts +++ b/examples/advanced/hello-world.ts @@ -22,7 +22,6 @@ import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; expirationSec: 30 * 60, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", @@ -38,7 +37,7 @@ import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; }); const allocation = await glm.payment.createAllocation({ - budget: glm.market.estimateBudget(order), + budget: glm.market.estimateBudget({ order, concurrency: 1 }), expirationSec: 60 * 60, // 60 minutes }); const demandSpecification = await glm.market.buildDemandDetails(order.demand, allocation); diff --git a/examples/advanced/payment-filters.ts b/examples/advanced/payment-filters.ts index 0e31a9141..adcb8254a 100644 --- a/examples/advanced/payment-filters.ts +++ b/examples/advanced/payment-filters.ts @@ -37,7 +37,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/advanced/proposal-filter.ts b/examples/advanced/proposal-filter.ts index e5914d7ee..408c14ae5 100644 --- a/examples/advanced/proposal-filter.ts +++ b/examples/advanced/proposal-filter.ts @@ -16,7 +16,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/advanced/proposal-predefined-filter.ts b/examples/advanced/proposal-predefined-filter.ts index c228e4c86..b584393e4 100644 --- a/examples/advanced/proposal-predefined-filter.ts +++ b/examples/advanced/proposal-predefined-filter.ts @@ -13,7 +13,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/basic/many-of.ts b/examples/basic/many-of.ts index 647bab816..7b9f855fe 100644 --- a/examples/basic/many-of.ts +++ b/examples/basic/many-of.ts @@ -10,7 +10,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/basic/one-of.ts b/examples/basic/one-of.ts index 94d97b763..b53fe2716 100644 --- a/examples/basic/one-of.ts +++ b/examples/basic/one-of.ts @@ -6,7 +6,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/basic/run-and-stream.ts b/examples/basic/run-and-stream.ts index 4ff3af019..546f08aa1 100644 --- a/examples/basic/run-and-stream.ts +++ b/examples/basic/run-and-stream.ts @@ -11,7 +11,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/basic/transfer.ts b/examples/basic/transfer.ts index b3596d4dd..c0b005879 100644 --- a/examples/basic/transfer.ts +++ b/examples/basic/transfer.ts @@ -7,7 +7,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 2, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/basic/vpn.ts b/examples/basic/vpn.ts index 38bfdf227..d277aced8 100644 --- a/examples/basic/vpn.ts +++ b/examples/basic/vpn.ts @@ -16,7 +16,6 @@ import { pinoPrettyLogger } from "@golem-sdk/pino-logger"; workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 2, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/experimental/deployment/new-api.ts b/examples/experimental/deployment/new-api.ts index 93f1b87ab..42ff943db 100644 --- a/examples/experimental/deployment/new-api.ts +++ b/examples/experimental/deployment/new-api.ts @@ -25,7 +25,6 @@ async function main() { }, }, market: { - maxAgreements: 2, rentHours: 12, pricing: { model: "linear", @@ -53,7 +52,6 @@ async function main() { }, }, market: { - maxAgreements: 1, rentHours: 12 /* REQUIRED */, pricing: { model: "linear", diff --git a/examples/experimental/express/server.ts b/examples/experimental/express/server.ts index d71271bcb..b68c83374 100644 --- a/examples/experimental/express/server.ts +++ b/examples/experimental/express/server.ts @@ -30,7 +30,6 @@ app.post("/tts", async (req, res) => { }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/experimental/job/cancel.ts b/examples/experimental/job/cancel.ts index 038f18a50..e09740ae3 100644 --- a/examples/experimental/job/cancel.ts +++ b/examples/experimental/job/cancel.ts @@ -11,7 +11,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "severyn/espeak:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/experimental/job/getJobById.ts b/examples/experimental/job/getJobById.ts index 0454f59f8..01bf0c405 100644 --- a/examples/experimental/job/getJobById.ts +++ b/examples/experimental/job/getJobById.ts @@ -12,7 +12,6 @@ const order: MarketOrderSpec = { workload: { imageTag: "severyn/espeak:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/examples/experimental/job/waitForResults.ts b/examples/experimental/job/waitForResults.ts index 2813ec573..4d6fcbdbd 100644 --- a/examples/experimental/job/waitForResults.ts +++ b/examples/experimental/job/waitForResults.ts @@ -13,7 +13,6 @@ const order: MarketOrderSpec = { }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/src/experimental/deployment/builder.test.ts b/src/experimental/deployment/builder.test.ts index 58ab8aa3b..542709c51 100644 --- a/src/experimental/deployment/builder.test.ts +++ b/src/experimental/deployment/builder.test.ts @@ -20,7 +20,6 @@ describe("Deployment builder", () => { }, }, market: { - maxAgreements: 1, rentHours: 1, pricing: { model: "linear", @@ -29,6 +28,9 @@ describe("Deployment builder", () => { maxCpuPerHourPrice: 1, }, }, + deployment: { + replicas: 1, + }, }) .createLeaseProcessPool("my-pool", { demand: { @@ -40,7 +42,6 @@ describe("Deployment builder", () => { }, }, market: { - maxAgreements: 1, rentHours: 1, pricing: { model: "linear", @@ -49,6 +50,9 @@ describe("Deployment builder", () => { maxCpuPerHourPrice: 1, }, }, + deployment: { + replicas: 1, + }, }); }).toThrow(new GolemConfigError(`Lease Process Pool with name my-pool already exists`)); }); @@ -76,7 +80,6 @@ describe("Deployment builder", () => { workload: { imageTag: "image", minCpuCores: 1, minMemGib: 1, minStorageGib: 1 }, }, market: { - maxAgreements: 1, rentHours: 1, pricing: { model: "linear", @@ -87,6 +90,7 @@ describe("Deployment builder", () => { }, deployment: { network: "non-existing-network", + replicas: 1, }, }) .getDeployment(); diff --git a/src/experimental/deployment/builder.ts b/src/experimental/deployment/builder.ts index bdce34518..ff31313cf 100644 --- a/src/experimental/deployment/builder.ts +++ b/src/experimental/deployment/builder.ts @@ -5,12 +5,12 @@ import { GolemNetwork, MarketOrderSpec } from "../../golem-network"; import { validateDeployment } from "./validate-deployment"; interface DeploymentOptions { - replicas?: number | { min: number; max: number }; + replicas: number | { min: number; max: number }; network?: string; } export interface CreateLeaseProcessPoolOptions extends MarketOrderSpec { - deployment?: DeploymentOptions; + deployment: DeploymentOptions; } export class GolemDeploymentBuilder { diff --git a/src/experimental/deployment/deployment.ts b/src/experimental/deployment/deployment.ts index 938cd96ab..a4b6ed14e 100644 --- a/src/experimental/deployment/deployment.ts +++ b/src/experimental/deployment/deployment.ts @@ -132,7 +132,12 @@ export class Deployment { const longestExpiration = Math.max(...this.components.leaseProcessPools.map((pool) => pool.options.market.rentHours)) * 3600; const totalBudget = this.components.leaseProcessPools.reduce( - (acc, pool) => acc + this.modules.market.estimateBudget(pool.options), + (acc, pool) => + acc + + this.modules.market.estimateBudget({ + order: pool.options, + concurrency: pool.options.deployment.replicas, + }), 0, ); diff --git a/src/experimental/job/job.test.ts b/src/experimental/job/job.test.ts index 58bfc34a4..db7032985 100644 --- a/src/experimental/job/job.test.ts +++ b/src/experimental/job/job.test.ts @@ -29,7 +29,6 @@ describe("Job", () => { }, }, market: { - maxAgreements: 1, rentHours: 1, pricing: { model: "linear", diff --git a/src/golem-network/golem-network.test.ts b/src/golem-network/golem-network.test.ts index 2f82bcdd1..dea71d193 100644 --- a/src/golem-network/golem-network.test.ts +++ b/src/golem-network/golem-network.test.ts @@ -18,7 +18,6 @@ const order: MarketOrderSpec = Object.freeze({ workload: { imageTag: "golem/alpine:latest" }, }, market: { - maxAgreements: 1, rentHours: 0.5, pricing: { model: "linear", diff --git a/src/golem-network/golem-network.ts b/src/golem-network/golem-network.ts index f1a042767..cf8d537eb 100644 --- a/src/golem-network/golem-network.ts +++ b/src/golem-network/golem-network.ts @@ -12,7 +12,7 @@ import { IPaymentApi, PaymentModule, PaymentModuleImpl, PaymentModuleOptions } f import { ActivityModule, ActivityModuleImpl, IActivityApi, IFileServer } from "../activity"; import { Network, NetworkModule, NetworkModuleImpl, NetworkOptions, INetworkApi } from "../network"; import { EventEmitter } from "eventemitter3"; -import { LeaseProcess, LeaseProcessOptions, LeaseProcessPool, LeaseProcessPoolOptions } from "../lease-process"; +import { Concurrency, LeaseProcess, LeaseProcessOptions, LeaseProcessPool } from "../lease-process"; import { DebitNoteRepository, InvoiceRepository, MarketApiAdapter, PaymentApiAdapter } from "../shared/yagna"; import { ActivityApiAdapter } from "../shared/yagna/adapters/activity-api-adapter"; import { ActivityRepository } from "../shared/yagna/repository/activity-repository"; @@ -99,7 +99,7 @@ export interface GolemNetworkEvents { } interface ManyOfOptions { - concurrency: LeaseProcessPoolOptions["replicas"]; + concurrency: Concurrency; order: MarketOrderSpec; } @@ -296,7 +296,7 @@ export class GolemNetwork { logger: this.logger, }); - const budget = this.market.estimateBudget(order); + const budget = this.market.estimateBudget({ order, concurrency: 1 }); const allocation = await this.payment.createAllocation({ budget, expirationSec: order.market.rentHours * 60 * 60, @@ -381,7 +381,7 @@ export class GolemNetwork { logger: this.logger, }); - const budget = this.market.estimateBudget(order); + const budget = this.market.estimateBudget({ concurrency, order }); const allocation = await this.payment.createAllocation({ budget, expirationSec: order.market.rentHours * 60 * 60, diff --git a/src/lease-process/lease-process-pool.ts b/src/lease-process/lease-process-pool.ts index b5f31d679..17a34817c 100644 --- a/src/lease-process/lease-process-pool.ts +++ b/src/lease-process/lease-process-pool.ts @@ -19,8 +19,10 @@ export interface LeaseProcessPoolDependencies { logger: Logger; } +export type Concurrency = number | RequireAtLeastOne<{ min: number; max: number }>; + export interface LeaseProcessPoolOptions { - replicas?: number | RequireAtLeastOne<{ min: number; max: number }>; + replicas?: Concurrency; network?: Network; leaseProcessOptions?: LeaseProcessOptions; } diff --git a/src/market/market.module.test.ts b/src/market/market.module.test.ts index 545b706aa..8eb31f3cc 100644 --- a/src/market/market.module.test.ts +++ b/src/market/market.module.test.ts @@ -14,6 +14,7 @@ import { Allocation, IPaymentApi } from "../payment"; import { INetworkApi } from "../network/api"; import { NetworkModule } from "../network"; import { DraftOfferProposalPool } from "./draft-offer-proposal-pool"; +import { MarketOrderSpec } from "../golem-network"; const mockMarketApiAdapter = mock(MarketApiAdapter); const mockYagna = mock(YagnaApi); @@ -549,4 +550,81 @@ describe("Market module", () => { expect(marketModule.signAgreementFromPool(mockPool)).rejects.toThrow("Failed to acquire"); }); }); + describe("estimateBudget()", () => { + it("estimates budget for the exact concurrency level", () => { + const order: MarketOrderSpec = { + demand: { + workload: { + imageTag: "image", + minCpuThreads: 5, + }, + }, + market: { + rentHours: 5, + pricing: { + model: "linear", + maxStartPrice: 1, + maxEnvPerHourPrice: 2, + maxCpuPerHourPrice: 0.5, + }, + }, + }; + const concurrency = 3; + const cpuPrice = 0.5 * 5 * 5; // 5 threads for 0.5 per hour for 5 hours + const envPrice = 2 * 5; // 2 per hour for 5 hours + const totalPricePerMachine = 1 + cpuPrice + envPrice; + const expectedBudget = totalPricePerMachine * concurrency; + + const budget = marketModule.estimateBudget({ order, concurrency }); + expect(budget).toBeCloseTo(expectedBudget, 5); + }); + it("estimates budget for max concurrency level", () => { + const order: MarketOrderSpec = { + demand: { + workload: { + imageTag: "image", + minCpuThreads: 5, + }, + }, + market: { + rentHours: 5, + pricing: { + model: "linear", + maxStartPrice: 1, + maxEnvPerHourPrice: 2, + maxCpuPerHourPrice: 0.5, + }, + }, + }; + const concurrency = { max: 10 }; + const cpuPrice = 0.5 * 5 * 5; // 5 threads for 0.5 per hour for 5 hours + const envPrice = 2 * 5; // 2 per hour for 5 hours + const totalPricePerMachine = 1 + cpuPrice + envPrice; + const expectedBudget = totalPricePerMachine * concurrency.max; + + const budget = marketModule.estimateBudget({ order, concurrency }); + expect(budget).toBeCloseTo(expectedBudget, 5); + }); + it("estimates budget for non-linear pricing model", () => { + const order: MarketOrderSpec = { + demand: { + workload: { + imageTag: "image", + }, + }, + market: { + rentHours: 5, + pricing: { + model: "burn-rate", + avgGlmPerHour: 2, + }, + }, + }; + const concurrency = 3; + const expectedBudget = 5 * 2 * concurrency; + + const budget = marketModule.estimateBudget({ order, concurrency }); + expect(budget).toBeCloseTo(expectedBudget, 5); + }); + }); }); diff --git a/src/market/market.module.ts b/src/market/market.module.ts index 0a7c079e3..390a8dc02 100644 --- a/src/market/market.module.ts +++ b/src/market/market.module.ts @@ -29,15 +29,13 @@ import { GolemUserError } from "../shared/error/golem-error"; import { createAbortSignalFromTimeout } from "../shared/utils/abortSignal"; import { MarketOrderSpec } from "../golem-network"; import { NetworkModule, INetworkApi } from "../network"; +import { Concurrency } from "../lease-process"; export interface MarketEvents {} export type DemandEngine = "vm" | "vm-nvidia" | "wasmtime"; export interface MarketOptions { - /** The maximum number of agreements that you want to make with the market */ - maxAgreements: number; - /** How long you want to rent the resources in hours */ rentHours: number; @@ -147,11 +145,13 @@ export interface MarketModule { }): Observable; /** - * Provides a simple estimation of the budget that's required for given demand specification + * Estimate the budget for the given order and concurrency level. + * Keep in mind that this is just an estimate and the actual cost may vary. + * To get a more accurate estimate, make sure to specify an exact or maximum concurrency level. + * The method returns the estimated budget in GLM. * @param params */ - estimateBudget(params: MarketOrderSpec): number; - + estimateBudget({ concurrency, order }: { concurrency: Concurrency; order: MarketOrderSpec }): number; /** * Fetch the most up-to-date agreement details from the yagna */ @@ -463,17 +463,29 @@ export class MarketModuleImpl implements MarketModule { }); } - estimateBudget(params: MarketOrderSpec): number { - const pricingModel = params.market.pricing.model; + estimateBudget({ concurrency, order }: { concurrency: Concurrency; order: MarketOrderSpec }): number { + const pricingModel = order.market.pricing.model; // TODO: Don't assume for the user, at least not on pure golem-js level - const minCpuThreads = params.demand.workload?.minCpuThreads ?? 1; + const minCpuThreads = order.demand.workload?.minCpuThreads ?? 1; - const { rentHours, maxAgreements } = params.market; + const { rentHours } = order.market; + const maxAgreements = (() => { + if (typeof concurrency === "number") { + return concurrency; + } + if (concurrency.max) { + return concurrency.max; + } + if (concurrency.min) { + return concurrency.min; + } + return 1; + })(); switch (pricingModel) { case "linear": { - const { maxCpuPerHourPrice, maxStartPrice, maxEnvPerHourPrice } = params.market.pricing; + const { maxCpuPerHourPrice, maxStartPrice, maxEnvPerHourPrice } = order.market.pricing; const threadCost = maxAgreements * rentHours * minCpuThreads * maxCpuPerHourPrice; const startCost = maxAgreements * maxStartPrice; @@ -482,7 +494,7 @@ export class MarketModuleImpl implements MarketModule { return startCost + envCost + threadCost; } case "burn-rate": - return maxAgreements * rentHours * params.market.pricing.avgGlmPerHour; + return maxAgreements * rentHours * order.market.pricing.avgGlmPerHour; default: throw new GolemUserError(`Unsupported pricing model ${pricingModel}`); } diff --git a/tests/e2e/express.spec.ts b/tests/e2e/express.spec.ts index 1dbd3bc3f..6832eec47 100644 --- a/tests/e2e/express.spec.ts +++ b/tests/e2e/express.spec.ts @@ -34,7 +34,6 @@ describe("Express", function () { }, }, market: { - maxAgreements: 1, rentHours: 1, pricing: { model: "linear",