diff --git a/api/score.go b/api/score.go index d28b870..2eeca1b 100755 --- a/api/score.go +++ b/api/score.go @@ -1,15 +1,198 @@ package api +import ( + "fmt" + "math" + + "go.uber.org/zap" +) + +type ScoreReason struct { + Reason string + Offset float64 + Value interface{} + IsMultiplier bool +} + type ScoredOffer struct { - Offer Offer - Score float64 + Offer Offer + Score float64 + Reasons []ScoreReason +} + +var ( + sugar *zap.SugaredLogger +) + +func init() { + logger, err := zap.NewDevelopment() + sugar = logger.Sugar() + if err != nil { + panic(err) + } +} + +func AddReason(reasonList []ScoreReason, reason string, offset float64, multiplier bool, value interface{}) []ScoreReason { + return append(reasonList, ScoreReason{Reason: reason, Offset: offset, IsMultiplier: multiplier, Value: fmt.Sprintf("%v", value)}) } func ScoreOffers(offers []Offer) []ScoredOffer { var scoredOffers = make([]ScoredOffer, 0, len(offers)) + for _, offer := range offers { - score := 100.0 - scoredOffers = append(scoredOffers, ScoredOffer{Offer: offer, Score: score}) + reasons := make([]ScoreReason, 0, 16) + + // Judge cost + targetCost := 0.42 // $/hour + costMultiplier := targetCost / offer.Search.TotalHour // e.x $0.42 / $0.50 = x0.84 + // sugar.Debugw("Cost", "offer", offer.ID, "costMultiplier", costMultiplier, "targetCost", targetCost, "actualCost", offer.Search.TotalHour) + if math.Abs(costMultiplier-1.0) > 0.05 { + reasons = AddReason(reasons, "Cost Multiplier", costMultiplier, true, offer.Search.TotalHour) + } + + // Judge DLPerf + targetDLPerf := 85.0 + dlPerfMultiplier := offer.DLPerf / targetDLPerf // e.x 100 / 85 = x1.18 + if math.Abs(dlPerfMultiplier-1.0) > 0.03 { + if dlPerfMultiplier > 1.0 { + dlPerfMultiplier = math.Sqrt(dlPerfMultiplier) + } else { + dlPerfMultiplier = math.Pow(dlPerfMultiplier, 2.0) + } + reasons = AddReason(reasons, "DLPerf Multiplier", dlPerfMultiplier, true, offer.DLPerf) + } + + // Judge Internet Download Speed + if offer.InetDown < 150.0 { + reasons = AddReason(reasons, "Very Poor Internet Download Speed", -7.0, false, offer.InetDown) + } else if offer.InetDown < 300.0 { + reasons = AddReason(reasons, "Poor Internet Download Speed", -3.0, false, offer.InetDown) + } else if offer.InetDown < 500.0 { + reasons = AddReason(reasons, "Decent Internet Download Speed", 1.0, false, offer.InetDown) + } else if offer.InetDown < 1000.0 { + reasons = AddReason(reasons, "Good Internet Download Speed", 3.0, false, offer.InetDown) + } else if offer.InetDown >= 2000.0 { + reasons = AddReason(reasons, "Great Internet Download Speed", 4.0, false, offer.InetDown) + } + + // Judge Internet Upload Speed + if offer.InetUp < 50.0 { + reasons = AddReason(reasons, "Extremely Poor Internet Upload Speed", -9.0, false, offer.InetUp) + } else if offer.InetUp < 100.0 { + reasons = AddReason(reasons, "Very Poor Internet Upload Speed", -5.0, false, offer.InetUp) + } else if offer.InetUp < 200.0 { + reasons = AddReason(reasons, "Poor Internet Upload Speed", -3.0, false, offer.InetUp) + } else if offer.InetUp < 400.0 { + reasons = AddReason(reasons, "Decent Internet Upload Speed", 1.0, false, offer.InetUp) + } else if offer.InetUp >= 800.0 { + reasons = AddReason(reasons, "Great Internet Upload Speed", 3.0, false, offer.InetUp) + } else if offer.InetUp >= 1000.0 { + reasons = AddReason(reasons, "Amazing Internet Upload Speed", 4.0, false, offer.InetUp) + } + + // Judge verification + if offer.Verification == "verified" { + reasons = AddReason(reasons, "Verified", 2.0, false, offer.Verification) + } else { + reasons = AddReason(reasons, "Not Verified", -5.0, false, offer.Verification) + } + + // Judge direct port count + if offer.DirectPortCount < 8 { + reasons = AddReason(reasons, "Very low direct port count", -2.0, false, offer.DirectPortCount) + } else if offer.DirectPortCount >= 32 { + reasons = AddReason(reasons, "Decent port count", 0.5, false, offer.DirectPortCount) + } else if offer.DirectPortCount >= 100 { + reasons = AddReason(reasons, "High port count", 1.0, false, offer.DirectPortCount) + } + + // Judge CPU memory + if offer.CPURam < 16*1024 { + reasons = AddReason(reasons, "Low CPU memory", -4.0, false, offer.CPURam) + } else if offer.CPURam >= 31*1024 { + reasons = AddReason(reasons, "High CPU memory", 2.0, false, offer.CPURam) + } else if offer.CPURam >= 63*1024 { + reasons = AddReason(reasons, "Very High CPU memory", 3.0, false, offer.CPURam) + } + + // Judge GPU count + if offer.NumGPUs < 1 { + reasons = AddReason(reasons, "No GPUs", -20.0, false, offer.NumGPUs) + } else if offer.NumGPUs == 2 { + reasons = AddReason(reasons, "Dual GPU", 1.0, false, offer.NumGPUs) + } else if offer.NumGPUs >= 3 { + reasons = AddReason(reasons, "Multi GPU", 2.0, false, offer.NumGPUs) + } + + // Judge CUDA version + if offer.CudaMaxGood < 11.0 { + reasons = AddReason(reasons, "CUDA version very outdated", -7.0, false, offer.CudaMaxGood) + } else if offer.CudaMaxGood < 12.0 { + reasons = AddReason(reasons, "CUDA version outdated", -5.0, false, offer.CudaMaxGood) + } else if offer.CudaMaxGood >= 12.5 { + reasons = AddReason(reasons, "CUDA version high", 5.0, false, offer.CudaMaxGood) + } else if offer.CudaMaxGood >= 12.0 { + reasons = AddReason(reasons, "CUDA version decent", 1.5, false, offer.CudaMaxGood) + } + + // Judge effective core count + if offer.CPUCoresEffective < 4.0 { + reasons = AddReason(reasons, "Low core count", -5.0, false, offer.CPUCoresEffective) + } else if offer.CPUCoresEffective >= 8.0 { + reasons = AddReason(reasons, "High core count", 5.0, false, offer.CPUCoresEffective) + } else if offer.CPUCoresEffective >= 6.0 { + reasons = AddReason(reasons, "Decent core count", 2.0, false, offer.CPUCoresEffective) + } + + // Judge disk space available + if offer.DiskSpace < 100.0 { + reasons = AddReason(reasons, "Low disk space", -5.0, false, offer.DiskSpace) + } else if offer.DiskSpace >= 750.0 { + reasons = AddReason(reasons, "Reasonable disk space", 1.0, false, offer.DiskSpace) + } else if offer.DiskSpace >= 250.0 { + reasons = AddReason(reasons, "Concerning disk space", -1.0, false, offer.DiskSpace) + } else if offer.DiskSpace >= 2500.0 { + reasons = AddReason(reasons, "High disk space", 2.0, false, offer.DiskSpace) + } + + // Judge GPU architecture + if offer.GPUArch == "nvidia" { + reasons = AddReason(reasons, "Nvidia Preference", 1.0, false, offer.GPUArch) + } else { + reasons = AddReason(reasons, "Unknown/Incompatible GPU Architecture", -10.0, false, offer.GPUArch) + } + + // Judge reliability + if offer.Reliability2 < 0.98 { + reasons = AddReason(reasons, "Low reliability", -5.0, false, offer.Reliability2) + } else if offer.Reliability2 >= 0.999 { + reasons = AddReason(reasons, "Very high reliability", 5.0, false, offer.Reliability2) + } else if offer.Reliability2 >= 0.995 { + reasons = AddReason(reasons, "High reliability", 2.0, false, offer.Reliability2) + } else if offer.Reliability2 >= 0.99 { + reasons = AddReason(reasons, "Decent reliability", 1.0, false, offer.Reliability2) + } + + // Calculate base score + score := 0.0 + for _, reason := range reasons { + if !reason.IsMultiplier { + score += reason.Offset + } + } + + // Apply multipliers + multiplier := 1.0 + for _, reason := range reasons { + if reason.IsMultiplier { + multiplier *= reason.Offset + } + } + newScore := score * multiplier + // sugar.Infow("Multiplier Applied", "offer", offer.ID, "baseScore", score, "score", newScore, "multiplier", multiplier) + score = newScore + + scoredOffers = append(scoredOffers, ScoredOffer{Offer: offer, Score: score, Reasons: reasons}) } return scoredOffers } diff --git a/api/search.go b/api/search.go index 48b4bf0..fb5879e 100755 --- a/api/search.go +++ b/api/search.go @@ -6,6 +6,8 @@ import ( ) type AdvancedSearch struct { + Limit int `json:"limit,omitempty"` + AllocatedStorage float64 `json:"allocated_storage,omitempty"` Verified *bool `json:"verified,omitempty"` ComputeCap *ComparableInteger `json:"compute_cap,omitempty"` DiskSpace *ComparableInteger `json:"disk_space,omitempty"` @@ -50,6 +52,7 @@ type AdvancedSearch struct { func NewSearch() *AdvancedSearch { return &AdvancedSearch{ Rented: Pointer(false), + Limit: 500, } } diff --git a/api/types.go b/api/types.go index 807b635..a0085a0 100755 --- a/api/types.go +++ b/api/types.go @@ -101,67 +101,78 @@ type Instance struct { } type Offer struct { - IsBid bool `json:"is_bid"` - InetUpBilled *float64 `json:"inet_up_billed"` - InetDownBilled *float64 `json:"inet_down_billed"` - External bool `json:"external"` - Webpage *string `json:"webpage"` - Logo string `json:"logo"` - Rentable bool `json:"rentable"` - ComputeCap int `json:"compute_cap"` - DriverVersion string `json:"driver_version"` - CudaMaxGood float64 `json:"cuda_max_good"` - MachineID int `json:"machine_id"` - HostingType *float64 `json:"hosting_type"` - PublicIPAddr string `json:"public_ipaddr"` - Geolocation string `json:"geolocation"` - Geocode *int64 `json:"geolocode"` - FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"` - DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"` - Reliability2 float64 `json:"reliability2"` - HostRunTime int `json:"host_run_time"` - HostID int `json:"host_id"` - ID int `json:"id"` - BundleID int `json:"bundle_id"` - NumGPUs int `json:"num_gpus"` - TotalFlops float64 `json:"total_flops"` - MinBid float64 `json:"min_bid"` - DPHBase float64 `json:"dph_base"` - DPHTotal float64 `json:"dph_total"` - GPUName string `json:"gpu_name"` - GPURam int `json:"gpu_ram"` - GPUDisplayActive bool `json:"gpu_display_active"` - GPUMemBw float64 `json:"gpu_mem_bw"` - BwNVLink float64 `json:"bw_nvlink"` - DirectPortCount int `json:"direct_port_count"` - GPULanes int `json:"gpu_lanes"` - PCIeBw float64 `json:"pcie_bw"` - PCIGen float64 `json:"pci_gen"` - DLPerf float64 `json:"dlperf"` - CPUName string `json:"cpu_name"` - MoboName string `json:"mobo_name"` - CPURam int `json:"cpu_ram"` - CPUCores float64 `json:"cpu_cores"` - CPUCoresEffective float64 `json:"cpu_cores_effective"` - GPUFrac float64 `json:"gpu_frac"` - HasAVX int `json:"has_avx"` - DiskSpace float64 `json:"disk_space"` - DiskName string `json:"disk_name"` - DiskBw float64 `json:"disk_bw"` - InetUp float64 `json:"inet_up"` - InetDown float64 `json:"inet_down"` - StartDate float64 `json:"start_date"` - EndDate *float64 `json:"end_date"` - Duration *float64 `json:"duration"` - StorageCost float64 `json:"storage_cost"` - InetUpCost float64 `json:"inet_up_cost"` - InetDownCost float64 `json:"inet_down_cost"` - StorageTotalCost float64 `json:"storage_total_cost"` - Verification string `json:"verification"` - Score float64 `json:"score"` - Rented bool `json:"rented"` - BundledResults int `json:"bundled_results"` - PendingCount int `json:"pending_count"` + IsBid bool `json:"is_bid"` + InetUpBilled *float64 `json:"inet_up_billed"` + InetDownBilled *float64 `json:"inet_down_billed"` + External bool `json:"external"` + Webpage *string `json:"webpage"` + Logo string `json:"logo"` + Rentable bool `json:"rentable"` + ComputeCap int `json:"compute_cap"` + DriverVersion string `json:"driver_version"` + CudaMaxGood float64 `json:"cuda_max_good"` + MachineID int `json:"machine_id"` + HostingType *float64 `json:"hosting_type"` + PublicIPAddr string `json:"public_ipaddr"` + Geolocation string `json:"geolocation"` + Geocode *int64 `json:"geolocode"` + FlopsPerDPHTotal float64 `json:"flops_per_dphtotal"` + DLPerfPerDPHTotal float64 `json:"dlperf_per_dphtotal"` + Reliability2 float64 `json:"reliability2"` + HostRunTime int `json:"host_run_time"` + HostID int `json:"host_id"` + ID int `json:"id"` + BundleID int `json:"bundle_id"` + NumGPUs int `json:"num_gpus"` + TotalFlops float64 `json:"total_flops"` + MinBid float64 `json:"min_bid"` + DPHBase float64 `json:"dph_base"` + DPHTotal float64 `json:"dph_total"` + GPUName string `json:"gpu_name"` + GPUArch string `json:"gpu_arch"` + GPURam int `json:"gpu_ram"` + GPUDisplayActive bool `json:"gpu_display_active"` + GPUMemBw float64 `json:"gpu_mem_bw"` + BwNVLink float64 `json:"bw_nvlink"` + DirectPortCount int `json:"direct_port_count"` + GPULanes int `json:"gpu_lanes"` + PCIeBw float64 `json:"pcie_bw"` + PCIGen float64 `json:"pci_gen"` + DLPerf float64 `json:"dlperf"` + CPUName string `json:"cpu_name"` + MoboName string `json:"mobo_name"` + CPURam int `json:"cpu_ram"` + CPUCores float64 `json:"cpu_cores"` + CPUCoresEffective float64 `json:"cpu_cores_effective"` + GPUFrac float64 `json:"gpu_frac"` + HasAVX int `json:"has_avx"` + DiskSpace float64 `json:"disk_space"` + DiskName string `json:"disk_name"` + DiskBw float64 `json:"disk_bw"` + InetUp float64 `json:"inet_up"` + InetDown float64 `json:"inet_down"` + StartDate float64 `json:"start_date"` + EndDate *float64 `json:"end_date"` + Duration *float64 `json:"duration"` + StorageCost float64 `json:"storage_cost"` + InetUpCost float64 `json:"inet_up_cost"` + InetDownCost float64 `json:"inet_down_cost"` + StorageTotalCost float64 `json:"storage_total_cost"` + Verification string `json:"verification"` + Score float64 `json:"score"` + Rented bool `json:"rented"` + BundledResults int `json:"bundled_results"` + PendingCount int `json:"pending_count"` + Search ExtendedOfferDetails `json:"search"` + Instance ExtendedOfferDetails `json:"instance"` +} + +type ExtendedOfferDetails struct { + GPUCostPerHour float64 `json:"gpuCostPerHour"` + DiskHour float64 `json:"diskHour"` + TotalHour float64 `json:"totalHour"` + DiscountTotalHour float64 `json:"discountTotalHour"` + DiscountedTotalPerHour float64 `json:"discountPerHour"` } func (o *Offer) String() string { diff --git a/app.go b/app.go index 284b72f..0fd4076 100755 --- a/app.go +++ b/app.go @@ -52,6 +52,9 @@ func (a *App) Search() []api.ScoredOffer { // Create search search := api.NewSearch() + search.AllocatedStorage = 39.94657756485159 + search.Limit = 1000 + // search.Rentable = api.Pointer(true) // search.CPUCores = api.Ge(8) // Perform search diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dcf787b..fc94822 100755 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,8 +8,11 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "clsx": "^2.1.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-tooltip": "^5.28.0", + "tailwind-merge": "^2.5.5" }, "devDependencies": { "@types/react": "^18.0.17", @@ -394,6 +397,28 @@ "node": ">=12" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.8.tgz", + "integrity": "sha512-7XJ9cPU+yI2QeLS+FCSlqNFZJq8arvswefkZrYI1yQBbftw6FyrZOxYSh+9S7z7TpeWlRt9zJ5IhM1WIL334jA==", + "dependencies": { + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.12", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.12.tgz", + "integrity": "sha512-NP83c0HjokcGVEMeoStg317VD9W7eDlGK7457dMBANbKA6GJZdc7rjujdgqzTaz93jkGgc5P/jeWbaCHnMNc+w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.8" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.8.tgz", + "integrity": "sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -772,6 +797,19 @@ "node": ">= 6" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1985,6 +2023,19 @@ "node": ">=0.10.0" } }, + "node_modules/react-tooltip": { + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/react-tooltip/-/react-tooltip-5.28.0.tgz", + "integrity": "sha512-R5cO3JPPXk6FRbBHMO0rI9nkUG/JKfalBSQfZedZYzmqaZQgq7GLzF8vcCWx6IhUCKg0yPqJhXIzmIO5ff15xg==", + "dependencies": { + "@floating-ui/dom": "^1.6.1", + "classnames": "^2.3.0" + }, + "peerDependencies": { + "react": ">=16.14.0", + "react-dom": ">=16.14.0" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -2267,6 +2318,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz", + "integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.16", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz", diff --git a/frontend/package.json b/frontend/package.json index 72318f5..1f38323 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,8 +9,11 @@ "preview": "vite preview" }, "dependencies": { + "clsx": "^2.1.1", "react": "^18.2.0", - "react-dom": "^18.2.0" + "react-dom": "^18.2.0", + "react-tooltip": "^5.28.0", + "tailwind-merge": "^2.5.5" }, "devDependencies": { "@types/react": "^18.0.17", diff --git a/frontend/package.json.md5 b/frontend/package.json.md5 index 33d476b..ce0fe9e 100755 --- a/frontend/package.json.md5 +++ b/frontend/package.json.md5 @@ -1 +1 @@ -05b5c44c752e5fa7c8860e80963ac683 \ No newline at end of file +1a36b753b2917c4d0b27ca4c30b27a95 \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 274c215..4624822 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ function App() { async function invoke() { const offers = await Search(); console.log({ offer: offers[0] }); + offers.sort((a, b) => b.Score - a.Score); setState(offers); } @@ -18,10 +19,12 @@ function App() { return (