Skip to content
Merged
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
13 changes: 8 additions & 5 deletions pkg/provider/catalog/data/modal.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
6 changes: 4 additions & 2 deletions pkg/provider/modal/modal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions pkg/provider/modal/modal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
}
Loading