From 783db7e094a6d7393dd02e5dd6498a6539039c90 Mon Sep 17 00:00:00 2001 From: kerthcet Date: Sat, 15 Aug 2026 16:50:27 +0100 Subject: [PATCH] Pin GPU type Signed-off-by: kerthcet --- pkg/provider/catalog/data/modal.csv | 13 +++++--- pkg/provider/modal/modal.go | 6 ++-- pkg/provider/modal/modal_test.go | 51 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/pkg/provider/catalog/data/modal.csv b/pkg/provider/catalog/data/modal.csv index 2bd131a..d4aee2d 100644 --- a/pkg/provider/catalog/data/modal.csv +++ b/pkg/provider/catalog/data/modal.csv @@ -14,10 +14,11 @@ # # Columns (shared header across all provider CSVs; unused cells left blank): # accelerator_type canonical Nebula accelerator type -# accelerator_id BLANK — Modal names its GPUs exactly like the canonical types, -# so the mapping is identity and MapAccelerator falls back to -# accelerator_type. A provider whose ids diverge (AWS instance -# types) fills it in. +# accelerator_id USUALLY BLANK — Modal names its GPUs exactly like the canonical +# types, so the mapping is identity and MapAccelerator falls back +# to accelerator_type. A provider whose ids diverge (AWS instance +# types) fills it in. The one exception here is H100, which is +# pinned to "H100!" — see the note below. # gpu_count BLANK — Modal takes the accelerator count as a runtime # parameter (off the Pod's nvidia.com/gpu resource), so it is # not a catalog lookup dimension. AWS, where the count is baked @@ -34,5 +35,7 @@ A10G,,,OnDemand,1.10,true,,2026-07-25 L40S,,,OnDemand,1.95,true,,2026-07-25 A100-40GB,,,OnDemand,2.10,true,,2026-07-25 A100-80GB,,,OnDemand,2.50,true,,2026-07-25 -H100,,,OnDemand,3.95,true,,2026-07-25 +# "H100!", not "H100": Modal silently upgrades a bare H100 request onto an H200, and +# the "!" suffix is its documented opt-out. +H100,H100!,,OnDemand,3.95,true,,2026-07-25 H200,,,OnDemand,4.55,true,,2026-07-25 diff --git a/pkg/provider/modal/modal.go b/pkg/provider/modal/modal.go index 7ff8012..5dcb0cd 100644 --- a/pkg/provider/modal/modal.go +++ b/pkg/provider/modal/modal.go @@ -224,8 +224,10 @@ const ( // Provider is the Modal implementation of provider.Provider. It embeds // catalog.Base for the generic catalog methods (Name, Offerings, and the -// identity MapAccelerator — Modal names its GPUs exactly like Nebula's canonical -// names) and implements only the Modal-specific lifecycle here. +// catalog-driven MapAccelerator — Modal names its GPUs nearly identically to +// Nebula's canonical names, so most rows map by identity and the ones that don't +// carry an accelerator_id in modal.csv) and implements only the Modal-specific +// lifecycle here. type Provider struct { catalog.Base client Client diff --git a/pkg/provider/modal/modal_test.go b/pkg/provider/modal/modal_test.go index 1d59293..5093c89 100644 --- a/pkg/provider/modal/modal_test.go +++ b/pkg/provider/modal/modal_test.go @@ -30,6 +30,7 @@ import ( nebulav1alpha1 "github.com/InftyAI/Nebula/api/v1alpha1" "github.com/InftyAI/Nebula/pkg/provider" + "github.com/InftyAI/Nebula/pkg/provider/catalog" "github.com/InftyAI/Nebula/pkg/util" ) @@ -863,3 +864,53 @@ func TestModalProbe(t *testing.T) { } } } + +// Modal may silently place a bare gpu="H100" request on an H200, which would make +// Nebula's own bookkeeping lie: the optimizer picked the row on the H100 price, the +// blocklist keys failures on "H100:1", and the NodeClaim reports accelerator=H100 — +// while a workload benchmarking an H100 would measure a different card. modal.csv +// therefore pins the row's accelerator_id to Modal's documented opt-out, "H100!". +// This runs against the REAL embedded catalog, not a fake, because the pin IS the +// data — a fake would assert nothing about what ships. +func TestProvision_PinsH100AgainstAutoUpgrade(t *testing.T) { + cat, err := catalog.Load() + if err != nil { + t.Fatalf("load embedded catalog: %v", err) + } + f := &fakeClient{createID: "sb-1"} + p := New(f, cat) + + if _, err := p.Provision(context.Background(), gpuPod("claim-a", "H100", 2), + provider.ProvisionRequest{ClaimName: "claim-a"}); err != nil { + t.Fatalf("Provision: %v", err) + } + if f.lastSpec.GPU != "H100!" { + t.Fatalf("spec GPU = %q, want H100! — a bare H100 lets Modal substitute an H200", f.lastSpec.GPU) + } + // The suffix must survive the count suffix too: "H100!:2", not "H100:2!". + if got := gpuReservation(f.lastSpec.GPU, f.lastSpec.GPUCount); got != "H100!:2" { + t.Fatalf("gpuReservation = %q, want H100!:2", got) + } +} + +// The counterpart to the H100 pin: every other Modal row maps by identity. A100 is +// the one to watch — Modal upgrades a bare "A100" to the 80GB card, but the catalog +// never emits that token; it ships the already-exact A100-40GB/A100-80GB names. +func TestMapAccelerator_OtherRowsMapByIdentity(t *testing.T) { + cat, err := catalog.Load() + if err != nil { + t.Fatalf("load embedded catalog: %v", err) + } + p := New(&fakeClient{}, cat) + + for _, canonical := range []string{"T4", "L4", "A10G", "L40S", "A100-40GB", "A100-80GB", "H200"} { + ids, ok := p.MapAccelerator(canonical, 1) + if !ok || len(ids) != 1 || ids[0] != canonical { + t.Errorf("MapAccelerator(%q, 1) = (%v, %v), want ([%s], true)", canonical, ids, ok, canonical) + } + } + // And bare A100 is not an offering at all, so it can never reach Modal. + if ids, ok := p.MapAccelerator("A100", 1); ok { + t.Errorf("MapAccelerator(A100, 1) = (%v, true), want not-offered: the bare token is the fuzzy one", ids) + } +}