From e51aa53640265fdb71ea9667fcd36f70df07aff5 Mon Sep 17 00:00:00 2001 From: Rehaan Ahmad <34905129+rehaanahmad2013@users.noreply.github.com> Date: Sun, 14 Jun 2026 15:39:06 -0700 Subject: [PATCH] cpu sandboxes (#10) --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 4 ++-- SKILL.md | 18 +++++++++++----- src/client.rs | 28 +++++++++++++++++++++++++ src/commands/compute.rs | 46 ++++++++++++++++++++++++++++++++++++++--- src/main.rs | 8 +++++-- 7 files changed, 94 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 663c072c..dc2ab8ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -702,7 +702,7 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "openresearch-cli" -version = "0.1.20" +version = "0.1.21" dependencies = [ "anyhow", "clap", diff --git a/Cargo.toml b/Cargo.toml index b873070d..b4f516e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "openresearch-cli" -version = "0.1.20" +version = "0.1.21" edition = "2021" description = "OpenResearch CLI (orx) — Rust port" repository = "https://github.com/alphaXiv/openresearch-cli" diff --git a/README.md b/README.md index 6b65bc26..ea48a5ac 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,10 @@ Run the tests with `cargo test`. | `orx logout` | Removes the stored token. | | `orx install-skills [--agent claude\|codex\|opencode\|cursor\|all]` | Installs the `orx` skill shim into local coding agents (Claude Code, Codex, OpenCode, Cursor) so they auto-discover the CLI (`orx login` also offers this). | | `orx projects [--all]` | Lists your projects, grouped by organization. `--all` includes archived. | -| `orx compute [--gpu ] [--count ]` | Lists the GPU compute catalog, sorted by price. | +| `orx compute [--gpu ] [--count ] \| --cpu]` | Lists the GPU compute catalog, or CPU-only offers with `--cpu`, sorted by price. | | `orx exp status ` | Shows an experiment's status, branch, parent, run command, and latest run, plus a local `git diff` recipe for what the run changed. | | `orx exp cmd [--set ]` | Views or sets the experiment's run command. | -| `orx exp run (--gpu [--count ] [--disk ] \| --sandbox )` | Launches a run on new or existing compute. | +| `orx exp run (--gpu [--count ] [--disk ] \| --cpu [--vcpus ] \| --sandbox )` | Launches a run on new GPU, new CPU-only, or existing compute. | | `orx exp cancel ` | Cancels the in-flight run. | | `orx lit "" [--limit ] [--json]` | Full-text search alphaXiv's paper corpus (no login required). | | `orx paper [--full]` | Fetch a paper's machine-readable report, or its full text with `--full`. | diff --git a/SKILL.md b/SKILL.md index 245ea6ac..a0130950 100644 --- a/SKILL.md +++ b/SKILL.md @@ -86,7 +86,7 @@ orx logout # remove the stored token |---|---| | `orx create-project --name "" [--repo ]` | Create a project **and its baseline (root node)**: bound to a GitHub repo, or on a fresh blank repo when `--repo` is omitted. See below. | | `orx create-experiment --title "" [...]` | Add an experiment node; prints its git branch. See below. | -| `orx compute [--gpu ] [--count ]` | List the GPU compute catalog (price-sorted). See below. | +| `orx compute [--gpu ] [--count ] \| --cpu]` | List the GPU compute catalog, or CPU-only offers with `--cpu` (price-sorted). See below. | | `orx exp status/cmd/run/cancel/wait ` | Inspect, run, cancel, and wait on a single experiment node. `status` prints the node's branch, its parent's branch, the latest run's full commit SHA, and a ready-to-paste local `git diff` recipe. See below. | | `orx exp desc [--set "" \| --stdin]` | Read or overwrite the experiment's description (free-form notes). See below. | @@ -343,7 +343,8 @@ orx create-experiment --title "Baseline" Each experiment node has a **run command** (the shell command that trains/evaluates it) and is launched on **compute** you choose at run time. Compute is *not* stored -on the node — you pick a GPU (or an existing sandbox) each time you launch. +on the node — you pick a GPU, a CPU-only instance, or an existing sandbox each time +you launch. ```sh orx exp status # status, branch, parent, run command, latest run + commit, local diff recipe @@ -351,7 +352,9 @@ orx exp cmd # print the current run command orx exp cmd --set "bash run.sh" # set it ONCE on the baseline; children inherit it orx compute # browse GPU offers (price-sorted) orx compute --gpu H100_SXM --count 1 # filter the catalog -orx exp run --gpu H100_SXM --count 1 [--disk 100] # launch on a NEW instance +orx compute --cpu # browse CPU-only offers (price-sorted) +orx exp run --gpu H100_SXM --count 1 [--disk 100] # launch on a NEW GPU instance +orx exp run --cpu cpu5c --vcpus 8 # launch on a NEW CPU-only instance orx exp run --sandbox # launch on an EXISTING node orx exp cancel # cancel the in-flight run ``` @@ -370,14 +373,19 @@ Rules and notes: safety net, `orx exp run` refuses a child whose branch has **no changes over its parent** (the tell-tale of "queued before pushing") — push and retry, or pass `--force` to run the unchanged code deliberately. -- **Pick compute with exactly one of `--gpu` or `--sandbox`.** With `--gpu`, - `--count` defaults to `1` and `--disk` to `100` (GB). New instances are +- **Pick compute with exactly one of `--gpu`, `--cpu`, or `--sandbox`.** With + `--gpu`, `--count` defaults to `1` and `--disk` to `100` (GB). New instances are **RunPod-only** — the server picks the cheapest matching RunPod offer for the chosen (gpu, count); browse valid gpu ids and prices with `orx compute`. - **GPU ids are exact enum strings, not family names.** `--gpu H100` is invalid — the variant suffix is part of the id (`H100_SXM`, `H100_PCIE`, `A100_SXM_80GB`, `RTX_4090`, …). Use the exact `GPU` column value from `orx compute`; run it first if unsure. +- **Use `--cpu` for GPU-less work** (data prep, eval harnesses, CPU-bound papers). + The flavor is one of `cpu5c` (compute), `cpu5g` (general), or `cpu5m` (memory); + `--vcpus` is one of `2`, `8`, `32` (default `8`). Browse offers with + `orx compute --cpu`. CPU instances size their disk from the vCPU count, so there + is no `--disk` knob. - `orx exp run` **queues** the run and returns immediately — it does not wait. Follow progress with `orx runs ` and `orx logs `, or block with `orx exp wait` (below). diff --git a/src/client.rs b/src/client.rs index 590246ef..4bc2db82 100644 --- a/src/client.rs +++ b/src/client.rs @@ -111,6 +111,30 @@ pub struct ListCatalog { pub offers: Vec, } +/// A single CPU-only offer from the CPU catalog (`GET /compute/catalog/cpu`). +/// Sibling to [`GpuOffer`]; CPU instances live in their own RunPod-only catalog. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CpuOffer { + pub provider: String, + pub offer_id: String, + /// Flavor id: cpu5c (compute), cpu5g (general), or cpu5m (memory-optimized). + pub cpu_flavor: String, + /// Virtual CPUs allocated to the instance. + pub vcpus: f64, + /// System RAM in GB. + pub ram_gb: f64, + pub price_per_hour: f64, + /// Disk storage rate while running, USD per GB per hour. + pub disk_per_gb_hour: f64, + pub region: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ListCpuCatalog { + pub offers: Vec, +} + /// Response of `GET /experiments/{id}`: the experiment plus its most recent run. #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -707,6 +731,10 @@ pub async fn list_catalog(creds: &Credentials) -> Result { api_get(creds, "/compute/catalog").await } +pub async fn list_cpu_catalog(creds: &Credentials) -> Result { + api_get(creds, "/compute/catalog/cpu").await +} + pub async fn get_experiment(creds: &Credentials, exp_id: &str) -> Result { api_get(creds, &format!("/experiments/{}", exp_id)).await } diff --git a/src/commands/compute.rs b/src/commands/compute.rs index b3e3df69..236b6fc9 100644 --- a/src/commands/compute.rs +++ b/src/commands/compute.rs @@ -1,17 +1,25 @@ -//! The `compute` command. Lists the GPU offer catalog as a price-sorted table. +//! The `compute` command. Lists the GPU offer catalog as a price-sorted table, +//! or the CPU-only catalog with `--cpu`. //! //! The catalog endpoint spans every provider, but experiment runs launched on a //! *new* instance (`orx exp run --gpu ...`) are RunPod-only — the server //! resolves the cheapest matching RunPod offer for the chosen (gpu, count). So //! this command filters to RunPod offers: everything it lists is launchable. +//! CPU offers are RunPod-only to begin with, so no filtering is needed there. -use crate::client::list_catalog; +use crate::client::{list_catalog, list_cpu_catalog}; use crate::error::{require_credentials, Result}; use crate::output::print_table; -/// Lists the GPU compute catalog, optionally filtered by gpu id and/or count. +/// Lists the compute catalog. With `--cpu`, lists CPU-only offers; otherwise the +/// GPU catalog, optionally filtered by gpu id and/or count. pub async fn run(args: crate::ComputeArgs) -> Result<()> { let creds = require_credentials().await; + + if args.cpu { + return run_cpu(&creds).await; + } + let offers = list_catalog(&creds).await?.offers; // The API already returns offers sorted by price ascending; keep that order. @@ -56,3 +64,35 @@ pub async fn run(args: crate::ComputeArgs) -> Result<()> { Ok(()) } + +/// Lists the CPU-only offer catalog as a price-sorted table. Launch one with +/// `orx exp run --cpu --vcpus `. +async fn run_cpu(creds: &crate::config::Credentials) -> Result<()> { + let offers = list_cpu_catalog(creds).await?.offers; + + if offers.is_empty() { + println!("No CPU compute offers available."); + return Ok(()); + } + + let rows: Vec> = offers + .iter() + .map(|o| { + vec![ + o.cpu_flavor.clone(), + format!("{:.0}", o.vcpus), + format!("${:.2}", o.price_per_hour), + format!("{:.0}", o.ram_gb), + o.region.clone().unwrap_or_else(|| "—".to_string()), + o.provider.clone(), + ] + }) + .collect(); + + print_table( + &["FLAVOR", "VCPUS", "$/HR", "RAM(GB)", "REGION", "PROVIDER"], + &rows, + ); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index be1327e4..84ff477e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -248,10 +248,14 @@ pub struct CreateExperimentArgs { #[derive(Args, Debug)] pub struct ComputeArgs { - /// Filter to one GPU id (e.g. `H100_SXM`). Case-insensitive. + /// List CPU-only instance offers instead of the GPU catalog. CPU instances + /// suit GPU-less experiments (data prep, eval harnesses, CPU-bound papers). + #[arg(long)] + pub cpu: bool, + /// Filter to one GPU id (e.g. `H100_SXM`). Case-insensitive. GPU mode only. #[arg(long)] pub gpu: Option, - /// Filter to a specific GPU count per instance. + /// Filter to a specific GPU count per instance. GPU mode only. #[arg(long)] pub count: Option, }