Skip to content

Commit

Permalink
feat(deployment): move gpu-bot into the api
Browse files Browse the repository at this point in the history
  • Loading branch information
Redm4x authored Jan 24, 2025
1 parent 26c6ea8 commit 15217bf
Show file tree
Hide file tree
Showing 8 changed files with 366 additions and 3 deletions.
10 changes: 10 additions & 0 deletions apps/api/src/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { z } from "zod";
import { WalletController } from "@src/billing/controllers/wallet/wallet.controller";
import { chainDb } from "@src/db/dbConnection";
import { TopUpDeploymentsController } from "@src/deployment/controllers/deployment/deployment.controller";
import { GpuBotController } from "@src/deployment/controllers/gpu-bot/gpu-bot.controller";
import { UserController } from "@src/user/controllers/user/user.controller";
import { UserConfigService } from "@src/user/services/user-config/user-config.service";
import { ProviderController } from "./deployment/controllers/provider/provider.controller";
Expand Down Expand Up @@ -63,6 +64,15 @@ program
});
});

program
.command("gpu-pricing-bot")
.description("Create deployments for every gpu models to get up to date pricing information")
.action(async (options, command) => {
await executeCliHandler(command.name(), async () => {
await container.resolve(GpuBotController).createGpuBids();
});
});

const userConfig = container.resolve(UserConfigService);
program
.command("cleanup-stale-anonymous-users")
Expand Down
12 changes: 12 additions & 0 deletions apps/api/src/deployment/controllers/gpu-bot/gpu-bot.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { singleton } from "tsyringe";

import { GpuBidsCreatorService } from "@src/deployment/services/gpu-bids-creator/gpu-bids-creator.service";

@singleton()
export class GpuBotController {
constructor(private readonly gpuBidsCreatorService: GpuBidsCreatorService) {}

async createGpuBids() {
await this.gpuBidsCreatorService.createGpuBids();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import { MsgCloseDeployment, MsgCreateDeployment } from "@akashnetwork/akash-api/v1beta3";
import { SDL } from "@akashnetwork/akashjs/build/sdl";
import { getAkashTypeRegistry } from "@akashnetwork/akashjs/build/stargate";
import { LoggerService } from "@akashnetwork/logging";
import { DirectSecp256k1HdWallet, EncodeObject, Registry } from "@cosmjs/proto-signing";
import { calculateFee, SigningStargateClient } from "@cosmjs/stargate";
import axios from "axios";
import { TxRaw } from "cosmjs-types/cosmos/tx/v1beta1/tx";
import { singleton } from "tsyringe";

import { BillingConfig, InjectBillingConfig } from "@src/billing/providers";
import { getGpuModelsAvailability } from "@src/routes/v1/gpu";
import { RestAkashBidListResponseType } from "@src/types/rest";
import { apiNodeUrl } from "@src/utils/constants";
import { sleep } from "@src/utils/delay";
import { env } from "@src/utils/env";
import { sdlTemplateWithRam, sdlTemplateWithRamAndInterface } from "./sdl-templates";

@singleton()
export class GpuBidsCreatorService {
private readonly logger = LoggerService.forContext(GpuBidsCreatorService.name);

constructor(@InjectBillingConfig() private readonly config: BillingConfig) {}

async createGpuBids() {
if (!env.GPU_BOT_WALLET_MNEMONIC) throw new Error("The env variable GPU_BOT_WALLET_MNEMONIC is not set.");
if (!this.config.RPC_NODE_ENDPOINT) throw new Error("The env variable RPC_NODE_ENDPOINT is not set.");

const wallet = await DirectSecp256k1HdWallet.fromMnemonic(env.GPU_BOT_WALLET_MNEMONIC, { prefix: "akash" });
const [account] = await wallet.getAccounts();

this.logger.info("Wallet Address: " + account.address);

const myRegistry = new Registry([...getAkashTypeRegistry()]);

const client = await SigningStargateClient.connectWithSigner(this.config.RPC_NODE_ENDPOINT, wallet, {
registry: myRegistry,
broadcastTimeoutMs: 30_000
});
const balanceBefore = await client.getBalance(account.address, "uakt");
const balanceBeforeUAkt = parseFloat(balanceBefore.amount);
const akt = Math.round((balanceBeforeUAkt / 1_000_000) * 100) / 100;
this.logger.info("Balance: " + akt + "akt");

const gpuModels = await getGpuModelsAvailability();

await this.createBidsForAllModels(gpuModels, client, account.address, false);
await this.createBidsForAllModels(gpuModels, client, account.address, true);

const balanceAfter = await client.getBalance(account.address, "uakt");
const balanceAfterUAkt = parseFloat(balanceAfter.amount);
const diff = balanceBeforeUAkt - balanceAfterUAkt;

this.logger.info(`The operation cost ${diff / 1_000_000} akt`);
}

private async signAndBroadcast(address: string, client: SigningStargateClient, messages: readonly EncodeObject[]) {
const simulation = await client.simulate(address, messages, "");

const fee = calculateFee(Math.round(simulation * 1.35), "0.025uakt");

const txRaw = await client.sign(address, messages, fee, "");

const txRawBytes = Uint8Array.from(TxRaw.encode(txRaw).finish());
const txResult = await client.broadcastTx(txRawBytes);

if (txResult.code !== 0) {
this.logger.error(txResult);
throw new Error(`Error broadcasting transaction: ${txResult.rawLog}`);
}

return txResult;
}

private async createBidsForAllModels(
gpuModels: Awaited<ReturnType<typeof getGpuModelsAvailability>>,
client: SigningStargateClient,
walletAddress: string,
includeInterface: boolean
) {
const vendors = Object.keys(gpuModels.gpus.details).filter(x => x !== "<UNKNOWN>");

const models = vendors.flatMap(vendor => gpuModels.gpus.details[vendor].map(x => ({ vendor, ...x })));

models.sort(
(a, b) => a.vendor.localeCompare(b.vendor) || a.model.localeCompare(b.model) || a.ram.localeCompare(b.ram) || a.interface.localeCompare(b.interface)
);

this.logger.info(`Creating bids for every models (includeInterface: ${includeInterface})...`);

const doneModels: string[] = [];
for (const model of models) {
const dseq = (await this.getCurrentHeight()).toString();

this.logger.info(`Creating deployment for ${model.vendor} ${model.model} ${model.ram} ${model.interface}...`);

if (doneModels.includes(model.model + "-" + model.ram)) {
this.logger.info(" Skipping.");
continue;
}

const gpuSdl = this.getModelSdl(model.vendor, model.model, model.ram, includeInterface ? model.interface : undefined);

await this.createDeployment(client, gpuSdl, walletAddress, dseq);

this.logger.info("Done. Waiting for bids... ");

await sleep(30_000);

const bids = await this.getBids(walletAddress, dseq);

this.logger.info(`Got ${bids.bids.length} bids. Closing deployment...`);

await this.closeDeployment(client, walletAddress, dseq);

this.logger.info(" Done.");

if (!includeInterface) {
doneModels.push(model.model + "-" + model.ram);
}

await sleep(10_000);
}

this.logger.info("Finished!");
}

private async createDeployment(client: SigningStargateClient, sdlStr: string, owner: string, dseq: string) {
const sdl = SDL.fromString(sdlStr, "beta3");

const manifestVersion = await sdl.manifestVersion();
const message = {
typeUrl: `/akash.deployment.v1beta3.MsgCreateDeployment`,
value: MsgCreateDeployment.fromPartial({
id: {
owner: owner,
dseq: dseq
},
groups: sdl.groups(),
version: manifestVersion,
deposit: {
denom: "uakt",
amount: "500000" // 0.5 AKT
},
depositor: owner
})
};

await this.signAndBroadcast(owner, client, [message]);
}

private async closeDeployment(client: SigningStargateClient, owner: string, dseq: string) {
const message = {
typeUrl: `/akash.deployment.v1beta3.MsgCloseDeployment`,
value: MsgCloseDeployment.fromPartial({
id: {
owner: owner,
dseq: dseq
}
})
};

await this.signAndBroadcast(owner, client, [message]);
}

private async getBids(owner: string, dseq: string) {
const response = await axios.get<RestAkashBidListResponseType>(`${apiNodeUrl}/akash/market/v1beta4/bids/list?filters.owner=${owner}&filters.dseq=${dseq}`);

return response.data;
}

private getModelSdl(vendor: string, model: string, ram: string, gpuInterface?: string) {
let gpuSdl = gpuInterface ? sdlTemplateWithRamAndInterface : sdlTemplateWithRam;
gpuSdl = gpuSdl.replace("<VENDOR>", vendor);
gpuSdl = gpuSdl.replace("<MODEL>", model);
gpuSdl = gpuSdl.replace("<RAM>", ram);

if (gpuInterface) {
gpuSdl = gpuSdl.replace("<INTERFACE>", gpuInterface.toLowerCase().startsWith("sxm") ? "sxm" : gpuInterface.toLowerCase());
}

return gpuSdl;
}

private async getCurrentHeight() {
const response = await axios.get(`${apiNodeUrl}/blocks/latest`);

const height = parseInt(response.data.block.header.height);

if (isNaN(height)) throw new Error("Failed to get current height");

return height;
}
}
92 changes: 92 additions & 0 deletions apps/api/src/deployment/services/gpu-bids-creator/sdl-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
export const sdlTemplateWithRamAndInterface = `
version: "2.0"
services:
obtaingpuone:
image: ubuntu:22.04
command:
- "sh"
- "-c"
args:
- 'uptime;
nvidia-smi;
sleep infinity'
expose:
- port: 8080
as: 80
to:
- global: true
profiles:
compute:
obtaingpu:
resources:
cpu:
units: 0.1
memory:
size: 256Mi
gpu:
units: 1
attributes:
vendor:
<VENDOR>:
- model: <MODEL>
ram: <RAM>
interface: <INTERFACE>
storage:
size: 256Mi
placement:
akash:
pricing:
obtaingpu:
denom: uakt
amount: 100000
deployment:
obtaingpuone:
akash:
profile: obtaingpu
count: 1`;

export const sdlTemplateWithRam = `
version: "2.0"
services:
obtaingpuone:
image: ubuntu:22.04
command:
- "sh"
- "-c"
args:
- 'uptime;
nvidia-smi;
sleep infinity'
expose:
- port: 8080
as: 80
to:
- global: true
profiles:
compute:
obtaingpu:
resources:
cpu:
units: 0.1
memory:
size: 256Mi
gpu:
units: 1
attributes:
vendor:
<VENDOR>:
- model: <MODEL>
ram: <RAM>
storage:
size: 256Mi
placement:
akash:
pricing:
obtaingpu:
denom: uakt
amount: 100000
deployment:
obtaingpuone:
akash:
profile: obtaingpu
count: 1`;
10 changes: 8 additions & 2 deletions apps/api/src/routes/v1/gpu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ export default new OpenAPIHono().openapi(route, async c => {
}
}

const response = await getGpuModelsAvailability(vendor, model, memory_size, provider_address, provider_hosturi);

return c.json(response);
});

export async function getGpuModelsAvailability(vendor?: string, model?: string, memory_size?: string, provider_address?: string, provider_hosturi?: string) {
const gpuNodes = await chainDb.query<{
hostUri: string;
name: string;
Expand Down Expand Up @@ -159,5 +165,5 @@ export default new OpenAPIHono().openapi(route, async c => {
);
}

return c.json(response);
});
return response;
}
47 changes: 47 additions & 0 deletions apps/api/src/types/rest/akashBidListResponse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export type RestAkashBidListResponseType = {
bids: [
{
bid: {
bid_id: {
owner: string;
dseq: string;
gseq: number;
oseq: number;
provider: string;
};
state: "open" | "active" | "closed";
price: {
denom: string;
amount: string;
};
created_at: string;
};
escrow_account: {
id: {
scope: string;
xid: string;
};
owner: string;
state: string;
balance: {
denom: string;
amount: string;
};
transferred: {
denom: string;
amount: string;
};
settled_at: string;
depositor: string;
funds: {
denom: string;
amount: string;
};
};
}
];
pagination: {
next_key: string | null;
total: string;
};
};
1 change: 1 addition & 0 deletions apps/api/src/types/rest/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./akashBidListResponse";
export * from "./akashDeploymentListResponse";
export * from "./akashLeaseListResponse";
export * from "./akashDeploymentInfoResponse";
Expand Down
Loading

0 comments on commit 15217bf

Please sign in to comment.