feat: Add VastAI client
- Add a new VastAI client to the project. - This client allows users to search for and create GPU instances on VastAI. - It uses the VastAI API to interact with the platform. - Includes functionality for searching offers, getting top offers, and creating instances. Co-authored-by: mahmmoud.hassanein <mahmmoud.hassanein@gmail.com>
This commit is contained in:
24
examples/develop/vastai/vastai_example.vsh
Executable file
24
examples/develop/vastai/vastai_example.vsh
Executable file
@@ -0,0 +1,24 @@
|
|||||||
|
#!/usr/bin/env -S v -n -w -gc none -no-retry-compilation -cc tcc -d use_openssl -enable-globals run
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.clients.vastai
|
||||||
|
import json
|
||||||
|
import x.json2
|
||||||
|
|
||||||
|
// Create client with direct API key
|
||||||
|
// This uses VASTAI_API_KEY from environment
|
||||||
|
mut va := vastai.get()!
|
||||||
|
|
||||||
|
offers := va.search_offers()!
|
||||||
|
println('offers: ${offers}')
|
||||||
|
|
||||||
|
top_offers := va.get_top_offers(5)!
|
||||||
|
println('top offers: ${top_offers}')
|
||||||
|
|
||||||
|
create_instance_res := va.create_instance(
|
||||||
|
id: top_offers[0].id
|
||||||
|
config: vastai.CreateInstanceConfig{
|
||||||
|
image: 'pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime'
|
||||||
|
disk: 10
|
||||||
|
}
|
||||||
|
)!
|
||||||
|
println('create instance res: ${create_instance_res}')
|
||||||
8
lib/clients/vastai/.heroscript
Normal file
8
lib/clients/vastai/.heroscript
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
!!hero_code.generate_client
|
||||||
|
name:'vastai'
|
||||||
|
classname:'VastAI'
|
||||||
|
singleton:1
|
||||||
|
default:1
|
||||||
|
hasconfig:1
|
||||||
|
reset:0
|
||||||
173
lib/clients/vastai/client.v
Normal file
173
lib/clients/vastai/client.v
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
module vastai
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
// Represents a GPU offer from Vast.ai
|
||||||
|
pub struct GPUOffer {
|
||||||
|
pub:
|
||||||
|
id int // Unique instance ID
|
||||||
|
cuda_max_good int // Maximum reliable CUDA version
|
||||||
|
gpu_name string // Name of the GPU
|
||||||
|
gpu_ram int // GPU RAM in MB
|
||||||
|
num_gpus int // Number of GPUs
|
||||||
|
dlperf f64 // Deep Learning Performance score
|
||||||
|
dlperf_per_dphtotal f64 // Performance per dollar per hour
|
||||||
|
reliability f64 // Instance reliability score
|
||||||
|
total_flops f64 // Total FLOPS
|
||||||
|
credit_discount f64 // Credit discount
|
||||||
|
rented bool // Whether instance is currently rented
|
||||||
|
rentable bool // Whether instance can be rented
|
||||||
|
verification string // Verification status
|
||||||
|
external bool // Whether instance is external
|
||||||
|
dph_total f64 // Total dollars per hour
|
||||||
|
storage_total int // Total storage in GB
|
||||||
|
inet_up f64 // Upload bandwidth in Mbps
|
||||||
|
inet_down f64 // Download bandwidth in Mbps
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search parameters for filtering GPU offers
|
||||||
|
@[params]
|
||||||
|
pub struct SearchParams {
|
||||||
|
pub mut:
|
||||||
|
order ?string // Sort order (default: score descending)
|
||||||
|
query ?string // Search query string
|
||||||
|
min_gpu_ram ?int // Minimum GPU RAM in MB
|
||||||
|
min_num_gpus ?int // Minimum number of GPUs
|
||||||
|
min_dlperf ?f64 // Minimum deep learning performance score
|
||||||
|
max_dph ?f64 // Maximum dollars per hour
|
||||||
|
min_reliability ?f64 // Minimum reliability score
|
||||||
|
verified_only ?bool // Only show verified instances
|
||||||
|
external ?bool // Include external instances
|
||||||
|
rentable ?bool // Show only rentable instances
|
||||||
|
rented ?bool // Show only rented instances
|
||||||
|
}
|
||||||
|
|
||||||
|
// Response from the search API
|
||||||
|
struct SearchResponse {
|
||||||
|
success bool
|
||||||
|
offers []GPUOffer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searches for GPU offers based on the provided parameters
|
||||||
|
pub fn (mut va VastAI) search_offers(params SearchParams) ![]GPUOffer {
|
||||||
|
// Get HTTP client
|
||||||
|
mut http_client := va.httpclient()!
|
||||||
|
|
||||||
|
// Make request
|
||||||
|
resp := http_client.send(method: .put, prefix: '/search/asks/?', data: json.encode(params))!
|
||||||
|
|
||||||
|
if resp.code != 200 {
|
||||||
|
return error('request failed with code ${resp.code}: ${resp.data}')
|
||||||
|
}
|
||||||
|
// Parse response
|
||||||
|
search_resp := json.decode(SearchResponse, resp.data)!
|
||||||
|
|
||||||
|
return search_resp.offers
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to get top N offers sorted by performance/price
|
||||||
|
pub fn (mut v VastAI) get_top_offers(count int) ![]GPUOffer {
|
||||||
|
params := SearchParams{
|
||||||
|
order: 'dlperf_per_dphtotal-' // Sort by performance per dollar (descending)
|
||||||
|
rentable: true // Only show available instances
|
||||||
|
min_reliability: 0.98 // High reliability
|
||||||
|
}
|
||||||
|
|
||||||
|
offers := v.search_offers(params)!
|
||||||
|
|
||||||
|
if offers.len <= count {
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
return offers[..count]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to find cheapest offers meeting minimum requirements
|
||||||
|
pub fn (mut va VastAI) find_cheapest_offers(min_gpu_ram int, min_gpus int, count int) ![]GPUOffer {
|
||||||
|
params := SearchParams{
|
||||||
|
order: 'dph_total' // Sort by price (ascending)
|
||||||
|
min_gpu_ram: min_gpu_ram
|
||||||
|
min_num_gpus: min_gpus
|
||||||
|
rentable: true // Only show available instances
|
||||||
|
min_reliability: 0.95 // Reasonable reliability threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
offers := va.search_offers(params)!
|
||||||
|
|
||||||
|
if offers.len <= count {
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
return offers[..count]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to find most powerful GPUs available
|
||||||
|
pub fn (mut va VastAI) find_most_powerful(count int) ![]GPUOffer {
|
||||||
|
params := SearchParams{
|
||||||
|
order: 'dlperf-' // Sort by deep learning performance (descending)
|
||||||
|
rentable: true // Only show available instances
|
||||||
|
min_reliability: 0.95 // Reasonable reliability threshold
|
||||||
|
}
|
||||||
|
|
||||||
|
offers := va.search_offers(params)!
|
||||||
|
|
||||||
|
if offers.len <= count {
|
||||||
|
return offers
|
||||||
|
}
|
||||||
|
return offers[..count]
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstanceConfig represents the configuration for creating a new instance from an offer
|
||||||
|
@[params]
|
||||||
|
pub struct CreateInstanceConfig {
|
||||||
|
pub:
|
||||||
|
template_id ?string
|
||||||
|
template_hash_id ?string
|
||||||
|
image ?string // Docker image name
|
||||||
|
disk ?int
|
||||||
|
extra_env ?map[string]string // Environment variables
|
||||||
|
runtype ?string // "args" or "ssh"
|
||||||
|
onstart ?string
|
||||||
|
label ?string
|
||||||
|
image_login ?string
|
||||||
|
price ?f32
|
||||||
|
target_state ?string // "running" or "stopped"
|
||||||
|
cancel_unavail ?bool
|
||||||
|
vm ?bool
|
||||||
|
client_id ?string
|
||||||
|
apikey_id ?string
|
||||||
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct CreateInstanceArgs {
|
||||||
|
pub:
|
||||||
|
id int
|
||||||
|
config CreateInstanceConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstanceResponse represents the response from creating a new instance
|
||||||
|
pub struct CreateInstanceResponse {
|
||||||
|
pub:
|
||||||
|
success bool
|
||||||
|
new_contract int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new instance by accepting a provider offer
|
||||||
|
pub fn (mut va VastAI) create_instance(args CreateInstanceArgs) !CreateInstanceResponse {
|
||||||
|
// Get HTTP client
|
||||||
|
mut http_client := va.httpclient()!
|
||||||
|
|
||||||
|
// Make request
|
||||||
|
resp := http_client.send(
|
||||||
|
method: .put
|
||||||
|
prefix: '/asks/${args.id}/?'
|
||||||
|
data: json.encode(args.config)
|
||||||
|
)!
|
||||||
|
|
||||||
|
if resp.code != 200 {
|
||||||
|
return error('request failed with code ${resp.code}: ${resp.data}')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse response
|
||||||
|
instance_resp := json.decode(CreateInstanceResponse, resp.data)!
|
||||||
|
|
||||||
|
return instance_resp
|
||||||
|
}
|
||||||
30
lib/clients/vastai/readme.md
Normal file
30
lib/clients/vastai/readme.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# vastai
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
To get started
|
||||||
|
|
||||||
|
```vlang
|
||||||
|
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.clients. vastai
|
||||||
|
|
||||||
|
mut client:= vastai.get()!
|
||||||
|
|
||||||
|
client...
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
## example heroscript
|
||||||
|
|
||||||
|
```hero
|
||||||
|
!!vastai.configure
|
||||||
|
secret: '...'
|
||||||
|
host: 'localhost'
|
||||||
|
port: 8888
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
102
lib/clients/vastai/vastai_factory_.v
Normal file
102
lib/clients/vastai/vastai_factory_.v
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
module vastai
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.core.base
|
||||||
|
import freeflowuniverse.herolib.core.playbook
|
||||||
|
import freeflowuniverse.herolib.ui.console
|
||||||
|
|
||||||
|
__global (
|
||||||
|
vastai_global map[string]&VastAI
|
||||||
|
vastai_default string
|
||||||
|
)
|
||||||
|
|
||||||
|
/////////FACTORY
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct ArgsGet {
|
||||||
|
pub mut:
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn args_get(args_ ArgsGet) ArgsGet {
|
||||||
|
mut args := args_
|
||||||
|
if args.name == '' {
|
||||||
|
args.name = vastai_default
|
||||||
|
}
|
||||||
|
if args.name == '' {
|
||||||
|
args.name = 'default'
|
||||||
|
}
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get(args_ ArgsGet) !&VastAI {
|
||||||
|
mut args := args_get(args_)
|
||||||
|
if args.name !in vastai_global {
|
||||||
|
if args.name == 'default' {
|
||||||
|
if !config_exists(args) {
|
||||||
|
if default {
|
||||||
|
config_save(args)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config_load(args)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return vastai_global[args.name] or {
|
||||||
|
println(vastai_global)
|
||||||
|
panic('could not get config for vastai with name:${args.name}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_exists(args_ ArgsGet) bool {
|
||||||
|
mut args := args_get(args_)
|
||||||
|
mut context := base.context() or { panic('bug') }
|
||||||
|
return context.hero_config_exists('vastai', args.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_load(args_ ArgsGet) ! {
|
||||||
|
mut args := args_get(args_)
|
||||||
|
mut context := base.context()!
|
||||||
|
mut heroscript := context.hero_config_get('vastai', args.name)!
|
||||||
|
play(heroscript: heroscript)!
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_save(args_ ArgsGet) ! {
|
||||||
|
mut args := args_get(args_)
|
||||||
|
mut context := base.context()!
|
||||||
|
context.hero_config_set('vastai', args.name, heroscript_default()!)!
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set(o VastAI) ! {
|
||||||
|
mut o2 := obj_init(o)!
|
||||||
|
vastai_global[o.name] = &o2
|
||||||
|
vastai_default = o.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@[params]
|
||||||
|
pub struct PlayArgs {
|
||||||
|
pub mut:
|
||||||
|
heroscript string // if filled in then plbook will be made out of it
|
||||||
|
plbook ?playbook.PlayBook
|
||||||
|
reset bool
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn play(args_ PlayArgs) ! {
|
||||||
|
mut args := args_
|
||||||
|
|
||||||
|
if args.heroscript == '' {
|
||||||
|
args.heroscript = heroscript_default()!
|
||||||
|
}
|
||||||
|
mut plbook := args.plbook or { playbook.new(text: args.heroscript)! }
|
||||||
|
|
||||||
|
mut install_actions := plbook.find(filter: 'vastai.configure')!
|
||||||
|
if install_actions.len > 0 {
|
||||||
|
for install_action in install_actions {
|
||||||
|
mut p := install_action.params
|
||||||
|
cfg_play(p)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// switch instance to be used for vastai
|
||||||
|
pub fn switch(name string) {
|
||||||
|
vastai_default = name
|
||||||
|
}
|
||||||
59
lib/clients/vastai/vastai_model.v
Normal file
59
lib/clients/vastai/vastai_model.v
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
module vastai
|
||||||
|
|
||||||
|
import freeflowuniverse.herolib.data.paramsparser
|
||||||
|
import freeflowuniverse.herolib.core.httpconnection
|
||||||
|
import os
|
||||||
|
|
||||||
|
pub const version = '1.14.3'
|
||||||
|
const singleton = true
|
||||||
|
const default = true
|
||||||
|
|
||||||
|
// TODO: THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE TO STRUCT BELOW, IS STRUCTURED AS HEROSCRIPT
|
||||||
|
pub fn heroscript_default() !string {
|
||||||
|
heroscript := "
|
||||||
|
!!vastai.configure
|
||||||
|
name:'default'
|
||||||
|
api_key:'${os.getenv('VASTAI_API_KEY')}'
|
||||||
|
base_url:'https://console.vast.ai/api/v0/'
|
||||||
|
"
|
||||||
|
return heroscript
|
||||||
|
}
|
||||||
|
|
||||||
|
// THIS THE THE SOURCE OF THE INFORMATION OF THIS FILE, HERE WE HAVE THE CONFIG OBJECT CONFIGURED AND MODELLED
|
||||||
|
|
||||||
|
@[heap]
|
||||||
|
pub struct VastAI {
|
||||||
|
pub mut:
|
||||||
|
name string = 'default'
|
||||||
|
api_key string
|
||||||
|
base_url string
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cfg_play(p paramsparser.Params) ! {
|
||||||
|
// THIS IS EXAMPLE CODE AND NEEDS TO BE CHANGED IN LINE WITH struct above
|
||||||
|
mut mycfg := VastAI{
|
||||||
|
name: p.get_default('name', 'default')!
|
||||||
|
api_key: p.get_default('api_key', '${os.getenv('VASTAI_API_KEY')}')!
|
||||||
|
base_url: p.get_default('base_url', 'https://console.vast.ai/api/v0/')!
|
||||||
|
}
|
||||||
|
set(mycfg)!
|
||||||
|
}
|
||||||
|
|
||||||
|
fn obj_init(obj_ VastAI) !VastAI {
|
||||||
|
// never call get here, only thing we can do here is work on object itself
|
||||||
|
mut obj := obj_
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
fn (mut v VastAI) httpclient() !&httpconnection.HTTPConnection {
|
||||||
|
mut http_conn := httpconnection.new(
|
||||||
|
name: 'vastai_client_${v.name}'
|
||||||
|
url: v.base_url
|
||||||
|
cache: true
|
||||||
|
retry: 3
|
||||||
|
)!
|
||||||
|
http_conn.default_header.add(.authorization, 'Bearer ${v.api_key}')
|
||||||
|
http_conn.default_header.add(.accept, 'application/json')
|
||||||
|
|
||||||
|
return http_conn
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user