diff --git a/examples/develop/vastai/vastai_example.vsh b/examples/develop/vastai/vastai_example.vsh new file mode 100755 index 00000000..4df78dcd --- /dev/null +++ b/examples/develop/vastai/vastai_example.vsh @@ -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}') diff --git a/lib/clients/vastai/.heroscript b/lib/clients/vastai/.heroscript new file mode 100644 index 00000000..8c2e2d2b --- /dev/null +++ b/lib/clients/vastai/.heroscript @@ -0,0 +1,8 @@ + +!!hero_code.generate_client + name:'vastai' + classname:'VastAI' + singleton:1 + default:1 + hasconfig:1 + reset:0 \ No newline at end of file diff --git a/lib/clients/vastai/client.v b/lib/clients/vastai/client.v new file mode 100644 index 00000000..a8f6443e --- /dev/null +++ b/lib/clients/vastai/client.v @@ -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 +} diff --git a/lib/clients/vastai/readme.md b/lib/clients/vastai/readme.md new file mode 100644 index 00000000..6c68d3da --- /dev/null +++ b/lib/clients/vastai/readme.md @@ -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 +``` + + diff --git a/lib/clients/vastai/vastai_factory_.v b/lib/clients/vastai/vastai_factory_.v new file mode 100644 index 00000000..d36af444 --- /dev/null +++ b/lib/clients/vastai/vastai_factory_.v @@ -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 +} diff --git a/lib/clients/vastai/vastai_model.v b/lib/clients/vastai/vastai_model.v new file mode 100644 index 00000000..540206f0 --- /dev/null +++ b/lib/clients/vastai/vastai_model.v @@ -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 +}