Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
.lab/
.venv/
.env
.c3.local
*.rs.bk
.DS_Store
crates/lab-python/python/lab/_native.*
Expand All @@ -16,3 +17,6 @@ __pycache__/
.mypy_cache/
.pytest_cache/
.ruff_cache/
viewer/node_modules/
viewer/dist/
.claude/
56 changes: 54 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
resolver = "2"
members = [
"crates/lab-cli",
"crates/lab-compute",
"crates/lab-compiler",
"crates/lab-ide",
"crates/lab-ide-wasm",
Expand All @@ -11,6 +12,9 @@ members = [
"crates/lab-package",
"crates/lab-project",
"crates/lab-python",
"crates/lab-runfmt",
"crates/lab-runtime",
"crates/lab-scene",
]

[workspace.package]
Expand Down Expand Up @@ -60,6 +64,7 @@ pliron = { git = "https://github.com/pliron-org/pliron", rev = "a4390be145f151b1
# never links the native HID library; a runner re-enables `hid`.
byonoy-hid = { version = "0.1.0", default-features = false }
lab-compiler = { path = "crates/lab-compiler", version = "0.1.2" }
lab-compute = { path = "crates/lab-compute", version = "0.1.2" }
# Default features stay off at the workspace level so the compiler's pure
# protocol use never links libusb; the CLI runner re-enables `usb`.
hamilton-star = { version = "0.1.0", default-features = false }
Expand All @@ -72,6 +77,9 @@ lab-language-server = { path = "crates/lab-language-server", version = "0.1.2" }
opentrons-protocol = "0.1.0"
lab-package = { path = "crates/lab-package", version = "0.1.2" }
lab-project = { path = "crates/lab-project", version = "0.1.2" }
lab-runfmt = { path = "crates/lab-runfmt", version = "0.1.2" }
lab-runtime = { path = "crates/lab-runtime", version = "0.1.2" }
lab-scene = { path = "crates/lab-scene", version = "0.1.2" }

[workspace.lints.rust]
unsafe_code = "forbid"
Expand Down
6 changes: 4 additions & 2 deletions crates/lab-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@ anyhow.workspace = true
clap.workspace = true
flate2 = "1.1.9"
lab-compiler.workspace = true
lab-compute.workspace = true
lab-runfmt.workspace = true
# The runner needs the live USB transport the workspace dependency keeps
# off by default.
hamilton-star = { workspace = true, features = ["usb"] }
lab-instruments.workspace = true
lab-runtime = { workspace = true, features = ["hardware"] }
lab-scene.workspace = true
lab-package.workspace = true
lab-project.workspace = true
self-replace = "1.5.0"
Expand Down
15 changes: 15 additions & 0 deletions crates/lab-cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,19 @@ lab metadata --json
lab check --json
```

Remote robot-learning compute is C3-first. A local `.env` may hold
`C3_API_KEY`; it is ignored by Git and read as data rather than sourced as a
shell script. The doctor is read-only: it validates authentication and the
current L40-class catalog without submitting a job.

```sh
lab compute doctor
lab compute list
```

Isaac Sim requires RTX hardware, so the doctor recognizes C3's L40/L40S class
and does not present A100 or H100 capacity as Isaac-compatible. Actual training
commands remain absent until the tracked C3 capability gate and a real PPO
runner have passed.

Path dependencies may optionally carry a semver requirement, which is checked against the dependency manifest. Registry dependencies remain explicitly unsupported and fail closed; adding them requires a registry protocol and integrity model rather than silent fallback. Workflow execution commands will be added only when the durable runtime has real run semantics.
141 changes: 141 additions & 0 deletions crates/lab-cli/src/compute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! User-facing read-only checks for Lab's C3-first compute boundary.

use std::{env, ffi::OsString, path::PathBuf};

use anyhow::{Context, Result};
use lab_compute::{
ComputeProvider, HardwareCatalog, HardwareProfile,
c3::{C3Provider, dotenv_value},
};
use serde::Serialize;

use crate::Output;

#[derive(Debug, Serialize)]
struct ComputeDoctorReport {
provider: &'static str,
authenticated: bool,
catalog_profiles: usize,
isaac_compatible_profiles: Vec<HardwareProfile>,
}

fn provider(env_file: PathBuf) -> Result<C3Provider> {
let program = env::var_os("LAB_C3_BIN").unwrap_or_else(|| OsString::from("c3"));
let mut provider = C3Provider::new(program);
if env_file.exists() {
let api_key = dotenv_value(&env_file, "C3_API_KEY")?
.with_context(|| format!("{} has no non-empty C3_API_KEY", env_file.display()))?;
provider = provider.with_api_key(api_key);
}
Ok(provider)
}

fn isaac_compatible(profile: &HardwareProfile) -> bool {
profile.available
&& profile.accelerator == "cuda"
&& profile
.accelerator_memory_gb
.is_some_and(|memory| memory >= 16)
&& profile.selector.to_ascii_lowercase().starts_with("l40")
}

pub(crate) fn doctor(env_file: PathBuf, output: &Output) -> Result<()> {
let provider = provider(env_file)?;
provider
.authenticate()
.context("C3 authentication failed")?;
let catalog = provider
.hardware_catalog()
.context("failed to read C3 hardware catalog")?;
let compatible = catalog
.profiles
.iter()
.filter(|profile| isaac_compatible(profile))
.cloned()
.collect::<Vec<_>>();
if compatible.is_empty() {
anyhow::bail!(
"C3 authentication succeeded, but no L40-class Isaac-compatible profile is catalogued"
);
}
let names = compatible
.iter()
.map(|profile| profile.display_name.as_str())
.collect::<Vec<_>>()
.join(", ");
output.success(
"compute-doctor",
ComputeDoctorReport {
provider: provider.name(),
authenticated: true,
catalog_profiles: catalog.profiles.len(),
isaac_compatible_profiles: compatible,
},
format!(
"C3 authentication and catalog access passed\n Isaac-compatible: {names}\n no job was submitted"
),
)
}

pub(crate) fn list(env_file: PathBuf, output: &Output) -> Result<()> {
let provider = provider(env_file)?;
let catalog = provider
.hardware_catalog()
.context("failed to read C3 hardware catalog")?;
let human = catalog_table(&catalog);
output.success("compute-catalog", catalog, human)
}

fn catalog_table(catalog: &HardwareCatalog) -> String {
let mut lines = vec![format!("{} hardware", catalog.provider.to_uppercase())];
for profile in &catalog.profiles {
let memory = profile
.accelerator_memory_gb
.map(|value| format!("{value} GB"))
.unwrap_or_else(|| "n/a".to_owned());
let price = profile
.price_per_hour
.zip(profile.price_currency.as_deref())
.map(|(value, currency)| format!("{value:.3} {currency}/h"))
.unwrap_or_else(|| "price unavailable".to_owned());
let availability = profile.availability.as_deref().unwrap_or("unknown");
let isaac = if isaac_compatible(profile) {
" [Isaac-compatible class]"
} else {
""
};
lines.push(format!(
" {:<10} {:<20} {:<8} {:<18} {}{}",
profile.selector, profile.display_name, memory, price, availability, isaac
));
}
lines.join("\n")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn only_l40_cuda_profiles_are_accepted_for_isaac() {
let profile = |selector: &str, accelerator: &str, memory| HardwareProfile {
selector: selector.to_owned(),
display_name: selector.to_owned(),
accelerator: accelerator.to_owned(),
accelerator_count: 1,
accelerator_memory_gb: memory,
available: true,
availability: Some("high".to_owned()),
price_per_hour: None,
price_currency: None,
};
assert!(isaac_compatible(&profile("l40", "cuda", Some(48))));
assert!(isaac_compatible(&profile("l40s", "cuda", Some(48))));
assert!(!isaac_compatible(&profile("a100", "cuda", Some(80))));
assert!(!isaac_compatible(&profile("h100", "cuda", Some(80))));
assert!(!isaac_compatible(&profile("l40", "none", Some(48))));
let mut unavailable = profile("l40", "cuda", Some(48));
unavailable.available = false;
assert!(!isaac_compatible(&unavailable));
}
}
Loading
Loading