diff --git a/pkg/solver/matcher/match.go b/pkg/solver/matcher/match.go index 702ead6f..3ff04c15 100644 --- a/pkg/solver/matcher/match.go +++ b/pkg/solver/matcher/match.go @@ -81,6 +81,50 @@ func (result ramMismatch) attributes() []attribute.KeyValue { } } +type vramMismatch struct { + resourceOffer data.ResourceOffer + jobOffer data.JobOffer +} + +func (_ vramMismatch) matched() bool { return false } +func (_ vramMismatch) message() string { return "did not match VRAM" } +func (result vramMismatch) attributes() []attribute.KeyValue { + var resourceOfferVRAMs []int + for _, gpu := range result.resourceOffer.Spec.GPUs { + resourceOfferVRAMs = append(resourceOfferVRAMs, gpu.VRAM) + } + + var jobOfferVRAMS []int + for _, gpu := range result.jobOffer.Spec.GPUs { + jobOfferVRAMS = append(jobOfferVRAMS, gpu.VRAM) + } + + return []attribute.KeyValue{ + attribute.String("match_result", fmt.Sprintf("%T", result)), + attribute.Bool("match_result.matched", result.matched()), + attribute.String("match_result.message", result.message()), + attribute.IntSlice("match_result.resource_offer.spec.gpus.vram", resourceOfferVRAMs), + attribute.IntSlice("match_result.job_offer.spec.gpus.vram", jobOfferVRAMS), + } +} + +type diskSpaceMismatch struct { + resourceOffer data.ResourceOffer + jobOffer data.JobOffer +} + +func (_ diskSpaceMismatch) matched() bool { return false } +func (_ diskSpaceMismatch) message() string { return "did not match disk space" } +func (result diskSpaceMismatch) attributes() []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.String("match_result", fmt.Sprintf("%T", result)), + attribute.Bool("match_result.matched", result.matched()), + attribute.String("match_result.message", result.message()), + attribute.Int("match_result.job_offer.spec.disk", result.jobOffer.Spec.Disk), + attribute.Int("match_result.resource_offer.spec.disk", result.resourceOffer.Spec.Disk), + } +} + type moduleIDError struct { resourceOffer data.ResourceOffer jobOffer data.JobOffer @@ -227,6 +271,34 @@ func matchOffers( } } + // Skip VRAM check when job offer does not request VRAM + if len(jobOffer.Spec.GPUs) > 0 { + // Mismatch if job offer requests VRAM but resource provider has none + if len(resourceOffer.Spec.GPUs) == 0 { + return &vramMismatch{ + jobOffer: jobOffer, + resourceOffer: resourceOffer, + } + } + + // Mismatch if job offer largest VRAM is greater than resource offer largest VRAM + largestResourceOfferVRAM := getLargestVRAM(resourceOffer.Spec.GPUs) + largestJobOfferVRAM := getLargestVRAM(jobOffer.Spec.GPUs) + if largestResourceOfferVRAM < largestJobOfferVRAM { + return &vramMismatch{ + jobOffer: jobOffer, + resourceOffer: resourceOffer, + } + } + } + + if resourceOffer.Spec.Disk < jobOffer.Spec.Disk { + return &diskSpaceMismatch{ + jobOffer: jobOffer, + resourceOffer: resourceOffer, + } + } + moduleID, err := data.GetModuleID(jobOffer.Module) if err != nil { return &moduleIDError{ @@ -295,6 +367,16 @@ func matchOffers( } } +func getLargestVRAM(gpus []data.GPUSpec) int { + largestVRAM := 0 + for _, gpu := range gpus { + if gpu.VRAM > largestVRAM { + largestVRAM = gpu.VRAM + } + } + return largestVRAM +} + func logMatch(result matchResult) { switch r := result.(type) { case offersMatched: diff --git a/pkg/solver/matcher/matcher_test.go b/pkg/solver/matcher/matcher_test.go index 5d5924e8..8d4599aa 100644 --- a/pkg/solver/matcher/matcher_test.go +++ b/pkg/solver/matcher/matcher_test.go @@ -19,6 +19,14 @@ func TestMatchOffers(t *testing.T) { CPU: 1000, GPU: 1000, RAM: 1024, + GPUs: []data.GPUSpec{ + { + Name: "NVIDIA RTX 4090", + Vendor: "NVIDIA", + VRAM: 24576, // MB + }, + }, + Disk: 2924295844659, // Bytes }, DefaultPricing: data.DealPricing{ InstructionPrice: 10, @@ -29,9 +37,11 @@ func TestMatchOffers(t *testing.T) { basicJobOffer := data.JobOffer{ Spec: data.MachineSpec{ - CPU: 1000, - GPU: 1000, - RAM: 1024, + CPU: 1000, + GPU: 1000, + RAM: 1024, + GPUs: []data.GPUSpec{}, + Disk: 0, // Bytes }, Mode: data.MarketPrice, Services: services, @@ -51,6 +61,13 @@ func TestMatchOffers(t *testing.T) { Path: "/lilypad_module.json.tmpl", } + sdxlModuleConfig := data.ModuleConfig{ + Name: "", + Repo: "https://github.com/Lilypad-Tech/lilypad-module-sdxl", + Hash: "v0.9-lilypad1", + Path: "/lilypad_module.json.tmpl", + } + testCases := []struct { name string resourceOffer func(offer data.ResourceOffer) data.ResourceOffer @@ -100,6 +117,96 @@ func TestMatchOffers(t *testing.T) { }, shouldMatch: false, }, + { + name: "VRAM match when job creator specifies VRAM", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.GPUs = []data.GPUSpec{{VRAM: 24576}} + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Module = sdxlModuleConfig + offer.Spec.GPUs = []data.GPUSpec{{VRAM: 20000}} + return offer + }, + shouldMatch: true, + }, + { + name: "VRAM match when job creator does not specify VRAM", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.GPUs = []data.GPUSpec{{VRAM: 24576}} + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Module = sdxlModuleConfig + offer.Spec.GPUs = []data.GPUSpec{} + return offer + }, + shouldMatch: true, + }, + { + name: "VRAM requested is more than resource offer VRAM", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.GPUs = []data.GPUSpec{{VRAM: 24576}} + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Module = sdxlModuleConfig + offer.Spec.GPUs = []data.GPUSpec{{VRAM: 40000}} + return offer + }, + shouldMatch: false, + }, + { + name: "VRAM requested but resource offer has none", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.GPUs = []data.GPUSpec{} + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Module = sdxlModuleConfig + offer.Spec.GPUs = []data.GPUSpec{{VRAM: 49152}} + return offer + }, + shouldMatch: false, + }, + { + name: "Disk space match when job creator specifies disk space", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.Disk = 2924295844659 // Bytes + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Module = sdxlModuleConfig + offer.Spec.Disk = 1000000000000 + return offer + }, + shouldMatch: true, + }, + { + name: "Disk space match when job creator does not specify disk space", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.Disk = 2924295844659 // Bytes + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Module = sdxlModuleConfig + offer.Spec.Disk = 0 // zero-value + return offer + }, + shouldMatch: true, + }, + { + name: "Disk space requested is more than resource offer disk space", + resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { + offer.Spec.Disk = 2924295844659 // Bytes + return offer + }, + jobOffer: func(offer data.JobOffer) data.JobOffer { + offer.Spec.Disk = 4000000000000 + return offer + }, + shouldMatch: false, + }, { name: "Resource provider supports module", resourceOffer: func(offer data.ResourceOffer) data.ResourceOffer { @@ -212,7 +319,7 @@ func TestMatchOffers(t *testing.T) { t.Run(tc.name, func(t *testing.T) { result := matchOffers(tc.resourceOffer(basicResourceOffer), tc.jobOffer(basicJobOffer)) if result.matched() != tc.shouldMatch { - t.Errorf("Expected match to be %v, but got %v", tc.shouldMatch, result) + t.Errorf("Expected match to be %v, but got %+v", tc.shouldMatch, result) } }) }