Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions crates/prism-lium-types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,9 +184,9 @@ impl Offer {
///
/// Accepts an exact-width host, a larger host when `requested > 1`, or a
/// multi-GPU host that advertises (or omits-min) GPU splitting. A 1-GPU
/// pin never takes a non-split 8× pack — except NCU, which is whole-host
/// only and is the live B200 champion path. 8×5090 is never a silent
/// fallback.
/// pin never takes a non-split 8× pack. NCU is whole-host only, so
/// `rent_count` may exceed the pin; the client skips that offer before
/// POST and tries later split-capable rows. 8×5090 is never posted.
#[must_use]
pub fn matches_gpu_count(&self, requested: u32) -> bool {
if self.allows_split_for(requested) {
Expand Down
130 changes: 117 additions & 13 deletions crates/prism-lium/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -815,10 +815,14 @@ impl EvalJobBackend for LiumClient {
prism_lium_types::effective_gpu_count(selected.gpu_count, &selected.gpu_type);
// Split hosts: requested width. NCU / non-split: whole host.
let rent_gpu_count = selected.rent_count(spec.gpu_count);
if pref.matches_pin("RTX 5090") && rent_gpu_count >= 8 && spec.gpu_count < 8 {
return Err(LiumError::Api(format!(
"abort: refusing {rent_gpu_count}× 5090 rent (no 8×5090 fallback)"
)));
// Never POST more GPUs than requested (Proof harvest default 1×).
// Skip this candidate — a later split 8× B200 can still rent 1×.
if rent_gpu_count > spec.gpu_count {
last_err = format!(
"abort: refusing {rent_gpu_count}× {} rent (requested {}; no whole-host upsell)",
selected.gpu_type, spec.gpu_count
);
continue 'offers;
}
loop {
info!(
Expand Down Expand Up @@ -1557,7 +1561,7 @@ mod tests {
}

#[tokio::test]
async fn provision_rents_whole_host_on_ncu_2x_b200() {
async fn provision_refuses_ncu_2x_b200_when_requesting_one() {
let server = MockServer::start().await;
mount_common(
&server,
Expand All @@ -1578,21 +1582,117 @@ mod tests {
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": "pod-ncu"})),
)
.expect(0)
.mount(&server)
.await;
let c = LiumClient::with_base_url("test-key", server.uri()).unwrap();
// HarvestLimits.gpu_count stays 1; rent_count would upsize NCU to 2.
let err = c.provision(&provision_spec()).await.unwrap_err();
assert!(
err.to_string().contains("abort:") && err.to_string().contains("requested 1"),
"got {err}"
);
assert_eq!(provision_spec().gpu_count, 1);
}

#[tokio::test]
async fn provision_refuses_8x_b200_when_requesting_one() {
let server = MockServer::start().await;
mount_common(
&server,
serde_json::json!([{
"id": "eight-b200",
"machine_name": "NVIDIA B200",
"gpu_count": 8,
"available_gpu_count": 8,
"min_gpu_count_for_rental": 1,
"ncu_profiling_enabled": true,
"price_per_gpu": 6.52
}]),
)
.await;
Mock::given(method("POST"))
.and(path("/executors/eight-b200/rent"))
.and(body_partial_json(serde_json::json!({"gpu_count": 8})))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": "pod-8x"})),
)
.expect(0)
.mount(&server)
.await;
let c = LiumClient::with_base_url("test-key", server.uri()).unwrap();
assert_eq!(provision_spec().gpu_count, 1);
let err = c.provision(&provision_spec()).await.unwrap_err();
assert!(
err.to_string().contains("abort:")
&& err.to_string().contains("requested 1")
&& (err.to_string().contains("8×") || err.to_string().contains("8x")),
"got {err}"
);
}

#[tokio::test]
async fn provision_skips_ncu_upsell_and_rents_split_1x() {
let server = MockServer::start().await;
mount_common(
&server,
serde_json::json!([
{
"id": "ncu-2x",
"machine_name": "NVIDIA B200",
"gpu_count": 2,
"available_gpu_count": 2,
"min_gpu_count_for_rental": 1,
"ncu_profiling_enabled": true,
"price_per_gpu": 5.5
},
{
"id": "eight-b200-idle",
"machine_name": "NVIDIA B200",
"gpu_count": 8,
"available_gpu_count": 8,
"price_per_gpu": 6.52
}
]),
)
.await;
Mock::given(method("POST"))
.and(path("/executors/ncu-2x/rent"))
.and(body_partial_json(serde_json::json!({"gpu_count": 2})))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": "pod-ncu"})),
)
.expect(0)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/executors/eight-b200-idle/rent"))
.and(body_partial_json(serde_json::json!({"gpu_count": 8})))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": "pod-8x"})),
)
.expect(0)
.mount(&server)
.await;
Mock::given(method("POST"))
.and(path("/executors/eight-b200-idle/rent"))
.and(body_partial_json(serde_json::json!({"gpu_count": 1})))
.respond_with(
ResponseTemplate::new(200).set_body_json(serde_json::json!({"id": "pod-split-1"})),
)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path("/pods/pod-ncu"))
.and(path("/pods/pod-split-1"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"id": "pod-ncu", "status": "RUNNING"})),
.set_body_json(serde_json::json!({"id": "pod-split-1", "status": "RUNNING"})),
)
.mount(&server)
.await;
let c = LiumClient::with_base_url("test-key", server.uri()).unwrap();
// HarvestLimits.gpu_count stays 1; rent_count upsizes NCU to 2.
let inst = c.provision(&provision_spec()).await.unwrap();
assert_eq!(inst.id, "pod-ncu");
assert_eq!(provision_spec().gpu_count, 1);
assert_eq!(inst.id, "pod-split-1");
}

#[tokio::test]
Expand Down Expand Up @@ -1683,9 +1783,13 @@ mod tests {
spec.gpu_count = 4;
let err = c.provision(&spec).await.unwrap_err();
assert!(
err.to_string().contains("8×5090")
|| err.to_string().contains("8x5090")
|| err.to_string().contains("no 8"),
err.to_string().contains("abort:")
&& err.to_string().contains("requested 4")
&& (err.to_string().contains("8×")
|| err.to_string().contains("8x")
|| err.to_string().contains("8×5090")
|| err.to_string().contains("8x5090")
|| err.to_string().contains("no 8")),
"got {err}"
);
}
Expand Down
Loading