Merge pull request #34 from freeflowuniverse/development_vastai

feat: Add VastAI client
This commit is contained in:
Omdanii
2025-01-26 14:02:09 +02:00
committed by GitHub
6 changed files with 625 additions and 0 deletions

View File

@@ -0,0 +1,66 @@
#!/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}')
attach_sshkey_to_instance_res := va.attach_sshkey_to_instance(
id: 1
ssh_key: "ssh-rsa AAAA..."
)!
println('attach sshkey to instance res: ${attach_sshkey_to_instance_res}')
stop_instance_res := va.stop_instance(
id: 1
state: "stopped"
)!
println('stop instance res: ${stop_instance_res}')
destroy_instance_res := va.destroy_instance(
id: 1
)!
println('destroy instance res: ${destroy_instance_res}')
// For some reason this method returns an error from their server, 500 ERROR
// (request failed with code 500: {"error":"server_error","msg":"Something went wrong on the server"})
launch_instance_res := va.launch_instance(
// Required
num_gpus: 1,
gpu_name: "RTX_3090",
image: 'vastai/tensorflow',
disk: 10,
region: "us-west",
// Optional
env: "user=7amada, home=/home/7amada",
)!
println('destroy instance res: ${launch_instance_res}')
start_instances_res := va.start_instances(
ids: [1, 2, 3]
)!
println('start instances res: ${start_instances_res}')
start_instance_res := va.start_instance(
id: 1
)!
println('start instance res: ${start_instance_res}')

View File

@@ -0,0 +1,8 @@
!!hero_code.generate_client
name:'vastai'
classname:'VastAI'
singleton:1
default:1
hasconfig:1
reset:0

360
lib/clients/vastai/client.v Normal file
View File

@@ -0,0 +1,360 @@
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
}
@[params]
pub struct StopInstanceArgs {
pub:
id int @[required]
state string
}
pub struct StopInstanceResponse {
pub:
success bool
msg string
}
// Stops a running container and updates its status to 'stopped'.
pub fn (mut va VastAI) stop_instance(args StopInstanceArgs) !StopInstanceResponse {
// Get HTTP client
mut http_client := va.httpclient()!
payload := {
'state': args.state
}
// Make request
resp := http_client.send(
method: .put
prefix: '/instances/${args.id}/?'
data: json.encode(payload)
)!
if resp.code != 200 {
return error('request failed with code ${resp.code}: ${resp.data}')
}
// Parse response
instance_resp := json.decode(StopInstanceResponse, resp.data)!
return instance_resp
}
@[params]
pub struct DestroyInstanceArgs {
pub:
id int @[required]
}
pub struct DestroyInstanceResponse {
pub:
success bool
msg string
}
// Destroys an instance.
pub fn (mut va VastAI) destroy_instance(args DestroyInstanceArgs) !DestroyInstanceResponse {
// Get HTTP client
mut http_client := va.httpclient()!
// Make request
resp := http_client.send(
method: .delete
prefix: '/instances/${args.id}/?'
)!
if resp.code != 200 {
return error('request failed with code ${resp.code}: ${resp.data}')
}
// Parse response
instance_resp := json.decode(DestroyInstanceResponse, resp.data)!
return instance_resp
}
@[params]
pub struct AttachSshKeyToInstanceArgs {
pub:
id int @[required]
ssh_key string
}
pub struct AttachSshKeyToInstanceResponse {
pub:
success bool
msg string
}
// Attach SSH Key to Instance
pub fn (mut va VastAI) attach_sshkey_to_instance(args AttachSshKeyToInstanceArgs) !AttachSshKeyToInstanceResponse {
// Get HTTP client
mut http_client := va.httpclient()!
payload := {
'ssh_key': args.ssh_key
}
// Make request
resp := http_client.send(
method: .post
prefix: '/instances/${args.id}/ssh/?'
data: json.encode(payload)
)!
if resp.code != 200 {
return error('request failed with code ${resp.code}: ${resp.data}')
}
// Parse response
instance_resp := json.decode(AttachSshKeyToInstanceResponse, resp.data)!
return instance_resp
}
@[params]
pub struct LaunchInstanceArgs {
pub:
num_gpus int @[required]
gpu_name string @[required]
region string @[required]
image string @[required]
disk int @[required]
env ?string
args ?[]string
}
// Launch an instance, This endpoint launches an instance based on the specified parameters, selecting the first available offer that meets the criteria.
pub fn (mut va VastAI) launch_instance(args LaunchInstanceArgs) !CreateInstanceResponse {
// Get HTTP client
mut http_client := va.httpclient()!
// Make request
resp := http_client.send(
method: .put
prefix: '/launch_instance/?'
data: json.encode(args)
)!
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
}
@[params]
pub struct StartInstancesArgs {
pub:
ids []int @[json: 'IDs'; required]
}
pub struct StartInstancesResponse {
pub:
success bool
msg string
}
// Start Instances, Start a list of instances specified by their IDs.
pub fn (mut va VastAI) start_instances(args StartInstancesArgs) !StartInstancesResponse {
// Get HTTP client
mut http_client := va.httpclient()!
// Make request
resp := http_client.send(
method: .post
prefix: '/instances/start'
data: json.encode(args)
)!
if resp.code != 200 {
return error('request failed with code ${resp.code}: ${resp.data}')
}
// Parse response
instance_resp := json.decode(StartInstancesResponse, resp.data)!
return instance_resp
}
@[params]
pub struct StartInstanceArgs {
pub:
id int @[required]
}
// Start Instance, Start an instance specified by its ID.
pub fn (mut va VastAI) start_instance(args StartInstanceArgs) !StartInstancesResponse {
return va.start_instances(StartInstancesArgs{ ids: [args.id] })
}

View 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
```

View 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
}

View 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
}