Skip to content

Commit

Permalink
fix apply
Browse files Browse the repository at this point in the history
  • Loading branch information
Flaque committed Sep 14, 2024
1 parent 9007548 commit d6be0ae
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 90 deletions.
48 changes: 24 additions & 24 deletions src/helpers/fetchers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,31 @@ import { apiClient } from "../apiClient";
import { logAndQuit } from "./errors";

export async function getContract(contractId: string) {
const api = await apiClient();
const { data, response } = await api.GET("/v0/contracts/{id}", {
params: {
path: { id: contractId },
},
});
if (!response.ok) {
return logAndQuit(`Failed to get contract: ${response.statusText}`);
}
return data;
const api = await apiClient();
const { data, response } = await api.GET("/v0/contracts/{id}", {
params: {
path: { id: contractId },
},
});
if (!response.ok) {
return logAndQuit(`Failed to get contract: ${response.statusText}`);
}
return data;
}

export async function getOrder(orderId: string) {
const api = await apiClient();
const { data, response, error } = await api.GET("/v0/orders/{id}", {
params: {
path: { id: orderId },
},
});
if (!response.ok) {
// @ts-ignore
if (error?.code === "order.not_found") {
return null;
}
return logAndQuit(`Failed to get order: ${response.statusText}`);
const api = await apiClient();
const { data, response, error } = await api.GET("/v0/orders/{id}", {
params: {
path: { id: orderId },
},
});
if (!response.ok) {
// @ts-ignore
if (error?.code === "order.not_found") {
return null;
}
return data;
}
return logAndQuit(`Failed to get order: ${response.statusText}`);
}
return data;
}
26 changes: 21 additions & 5 deletions src/helpers/price.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
export function pricePerGPUHourToTotalPrice(pricePerGPUHourInCenticents: number, durationSeconds: number, nodes: number, gpusPerNode: number) {
return Math.ceil(pricePerGPUHourInCenticents * durationSeconds / 3600 * nodes * gpusPerNode);
export function pricePerGPUHourToTotalPrice(
pricePerGPUHourInCenticents: number,
durationSeconds: number,
nodes: number,
gpusPerNode: number,
) {
return Math.ceil(
((pricePerGPUHourInCenticents * durationSeconds) / 3600) *
nodes *
gpusPerNode,
);
}

export function totalPriceToPricePerGPUHour(totalPriceInCenticents: number, durationSeconds: number, nodes: number, gpusPerNode: number) {
return totalPriceInCenticents / nodes / gpusPerNode / (durationSeconds / 3600);
}
export function totalPriceToPricePerGPUHour(
totalPriceInCenticents: number,
durationSeconds: number,
nodes: number,
gpusPerNode: number,
) {
return (
totalPriceInCenticents / nodes / gpusPerNode / (durationSeconds / 3600)
);
}
32 changes: 17 additions & 15 deletions src/helpers/waitingForOrder.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
import ora from "ora";
import chalk from "chalk";
import { getOrder } from "./fetchers";
import ora from "ora";
import { logAndQuit } from "./errors";
import { getOrder } from "./fetchers";

export async function waitForOrderToNotBePending(orderId: string) {
const spinner = ora(`Order ${orderId} - pending (this can take a moment)`).start();
const maxTries = 25;
for (let i = 0; i < maxTries; i++) {
const order = await getOrder(orderId);
const spinner = ora(
`Order ${orderId} - pending (this can take a moment)`,
).start();
const maxTries = 25;
for (let i = 0; i < maxTries; i++) {
const order = await getOrder(orderId);

if (order && order?.status !== "pending") {
spinner.text = `Order ${orderId} - ${order?.status}`;
spinner.succeed();
console.log(chalk.green("Order placed successfully"));
return order;
}
await new Promise((resolve) => setTimeout(resolve, 500));
if (order && order?.status !== "pending") {
spinner.text = `Order ${orderId} - ${order?.status}`;
spinner.succeed();
console.log(chalk.green("Order placed successfully"));
return order;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}

spinner.fail();
logAndQuit(`Order ${orderId} - possibly failed`);
spinner.fail();
logAndQuit(`Order ${orderId} - possibly failed`);
}
57 changes: 41 additions & 16 deletions src/lib/buy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@ import {
logLoginMessageAndQuit,
logSessionTokenExpiredAndQuit,
} from "../helpers/errors";
import {
pricePerGPUHourToTotalPrice,
totalPriceToPricePerGPUHour,
} from "../helpers/price";
import {
type Centicents,
centicentsToDollarsFormatted,
priceWholeToCenticents,
roundEndDate,
roundStartDate,
} from "../helpers/units";
import { waitForOrderToNotBePending } from "../helpers/waitingForOrder";
import type { Nullable } from "../types/empty";
import { formatDuration } from "./orders";
import { pricePerGPUHourToTotalPrice, totalPriceToPricePerGPUHour } from "../helpers/price";
import { GPUS_PER_NODE } from "./constants";
import { waitForOrderToNotBePending } from "../helpers/waitingForOrder";
import { formatDuration } from "./orders";

dayjs.extend(relativeTime);
dayjs.extend(duration);
Expand Down Expand Up @@ -74,7 +77,9 @@ async function buyOrderAction(options: SfBuyOptions) {

if (accelerators % GPUS_PER_NODE !== 0) {
const exampleCommand = `sf buy -n ${GPUS_PER_NODE} -d "${options.duration}"`;
return logAndQuit(`At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`);
return logAndQuit(
`At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`,
);
}
const quantity = Math.ceil(accelerators / GPUS_PER_NODE);

Expand All @@ -89,10 +94,15 @@ async function buyOrderAction(options: SfBuyOptions) {
priceCenticents = priceParsed;
}

// Convert the price to the total price of the contract
// Convert the price to the total price of the contract
// (price per gpu hour * gpus per node * quantity * duration in hours)
if (priceCenticents) {
priceCenticents = pricePerGPUHourToTotalPrice(priceCenticents, durationSeconds, quantity, GPUS_PER_NODE);
priceCenticents = pricePerGPUHourToTotalPrice(
priceCenticents,
durationSeconds,
quantity,
GPUS_PER_NODE,
);
}

const yesFlagOmitted = options.yes === undefined || options.yes === null;
Expand All @@ -117,9 +127,20 @@ async function buyOrderAction(options: SfBuyOptions) {
}

const priceLabelUsd = c.green(centicentsToDollarsFormatted(quote.price));
const priceLabelPerGPUHour = c.green(centicentsToDollarsFormatted(totalPriceToPricePerGPUHour(quote.price, durationSeconds, quantity, GPUS_PER_NODE)));
const priceLabelPerGPUHour = c.green(
centicentsToDollarsFormatted(
totalPriceToPricePerGPUHour(
quote.price,
durationSeconds,
quantity,
GPUS_PER_NODE,
),
),
);

console.log(`This order is projected to cost ${priceLabelUsd} total or ${priceLabelPerGPUHour} per GPU hour`);
console.log(
`This order is projected to cost ${priceLabelUsd} total or ${priceLabelPerGPUHour} per GPU hour`,
);
} else {
// quote if no price was provided
if (!priceCenticents) {
Expand Down Expand Up @@ -292,17 +313,21 @@ function confirmPlaceOrderMessage(options: BuyOptions) {
timeDescription = `from ${startAtLabel} (${c.green(fromNowTime)}) until ${endsAtLabel}`;
}

const durationInSeconds = options.endsAt.getTime() / 1000 - options.startsAt.getTime() / 1000;
const pricePerGPUHour = totalPriceToPricePerGPUHour(options.priceCenticents, durationInSeconds, options.quantity, GPUS_PER_NODE);
const pricePerHourLabel = c.green(centicentsToDollarsFormatted(pricePerGPUHour));
const durationInSeconds =
options.endsAt.getTime() / 1000 - options.startsAt.getTime() / 1000;
const pricePerGPUHour = totalPriceToPricePerGPUHour(
options.priceCenticents,
durationInSeconds,
options.quantity,
GPUS_PER_NODE,
);
const pricePerHourLabel = c.green(
centicentsToDollarsFormatted(pricePerGPUHour),
);

const topLine = `${totalNodesLabel} ${instanceTypeLabel} ${nodesLabel} (${GPUS_PER_NODE * options.quantity} GPUs) at ${pricePerHourLabel} per GPU hour for ${c.green(durationHumanReadable)} ${timeDescription}`;

const dollarsLabel = c.green(
centicentsToDollarsFormatted(
pricePerGPUHour,
),
);
const dollarsLabel = c.green(centicentsToDollarsFormatted(pricePerGPUHour));

const gpusLabel = c.green(options.quantity * GPUS_PER_NODE);

Expand Down
2 changes: 1 addition & 1 deletion src/lib/constants.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const GPUS_PER_NODE = 8;
export const GPUS_PER_NODE = 8;
63 changes: 34 additions & 29 deletions src/lib/sell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,16 @@ import {
logLoginMessageAndQuit,
logSessionTokenExpiredAndQuit,
} from "../helpers/errors";
import { getContract } from "../helpers/fetchers";
import { pricePerGPUHourToTotalPrice } from "../helpers/price";
import {
priceWholeToCenticents,
roundEndDate,
roundStartDate,
} from "../helpers/units";
import type { PlaceSellOrderParameters } from "./orders";
import { GPUS_PER_NODE } from "./constants";
import { pricePerGPUHourToTotalPrice } from "../helpers/price";
import ora from "ora";
import chalk from "chalk";
import { getContract, getOrder } from "../helpers/fetchers";
import { waitForOrderToNotBePending } from "../helpers/waitingForOrder";
import { GPUS_PER_NODE } from "./constants";
import type { PlaceSellOrderParameters } from "./orders";

export function registerSell(program: Command) {
program
Expand All @@ -29,14 +27,8 @@ export function registerSell(program: Command) {
.requiredOption("-p, --price <price>", "The price in dollars, per GPU hour")
.requiredOption("-c, --contract-id <id>", "Specify the contract ID")
.option("-n, --accelerators <quantity>", "Specify the number of GPUs", "8")
.option(
"-s, --start <start>",
"Specify the start time (ISO 8601 format)",
)
.option(
"-d, --duration <duration>",
"Specify the duration in seconds",
)
.option("-s, --start <start>", "Specify the start time (ISO 8601 format)")
.option("-d, --duration <duration>", "Specify the duration in seconds")
.option(
"-f, --flags <flags>",
"Specify additional flags as JSON",
Expand All @@ -54,15 +46,16 @@ function forceAsNumber(value: string | number): number {
return Number.parseFloat(value);
}


function contractStartAndEnd(contract: {
shape: {
intervals: string[] // date strings
quantities: number[]
}
intervals: string[]; // date strings
quantities: number[];
};
}) {
const startDate = dayjs(contract.shape.intervals[0]).toDate();
const endDate = dayjs(contract.shape.intervals[contract.shape.intervals.length - 1]).toDate();
const endDate = dayjs(
contract.shape.intervals[contract.shape.intervals.length - 1],
).toDate();

return { startDate, endDate };
}
Expand Down Expand Up @@ -92,22 +85,29 @@ async function placeSellOrder(options: {
}

if (contract?.status === "pending") {
return logAndQuit(`Contract ${options.contractId} is currently pending. Please try again in a few seconds.`);
return logAndQuit(
`Contract ${options.contractId} is currently pending. Please try again in a few seconds.`,
);
}

if (options.accelerators % GPUS_PER_NODE !== 0) {
const exampleCommand = `sf sell -n ${GPUS_PER_NODE} -c ${options.contractId}`;
return logAndQuit(`At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`);
return logAndQuit(
`At the moment, only entire-nodes are available, so you must have a multiple of ${GPUS_PER_NODE} GPUs. Example command:\n\n${exampleCommand}`,
);
}

const { startDate: contractStartDate, endDate: contractEndDate } = contractStartAndEnd({
shape: {
intervals: contract.shape.intervals,
quantities: contract.shape.quantities,
}
});
const { startDate: contractStartDate, endDate: contractEndDate } =
contractStartAndEnd({
shape: {
intervals: contract.shape.intervals,
quantities: contract.shape.quantities,
},
});

let startDate = options.start ? chrono.parseDate(options.start) : contractStartDate;
let startDate = options.start
? chrono.parseDate(options.start)
: contractStartDate;
if (!startDate) {
return logAndQuit("Invalid start date");
}
Expand All @@ -131,7 +131,12 @@ async function placeSellOrder(options: {
const totalDurationSecs = dayjs(endDate).diff(startDate, "s");
const nodes = Math.ceil(options.accelerators / GPUS_PER_NODE);

const totalPrice = pricePerGPUHourToTotalPrice(priceCenticents, totalDurationSecs, nodes, GPUS_PER_NODE);
const totalPrice = pricePerGPUHourToTotalPrice(
priceCenticents,
totalDurationSecs,
nodes,
GPUS_PER_NODE,
);

const params: PlaceSellOrderParameters = {
side: "sell",
Expand Down

0 comments on commit d6be0ae

Please sign in to comment.