vast-sd-cli 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/vast-sd +49 -0
- data/lib/vast-sd/config.rb +85 -0
- data/lib/vast-sd/start.rb +133 -0
- data/lib/vast-sd/vast.rb +33 -0
- data/lib/vast-sd/version.rb +3 -0
- metadata +119 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 14f381c408944e84b5dadeab890803dd4797958336eae022d78081273a05a20c
|
4
|
+
data.tar.gz: 7625775037c2ea2cdc5e6a6b943935c75ae11ba8178dab286d3210929c1c9f2a
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 4595119068f2499dba27358f94bd0e4d27f1f41707f8395ee43923d344e92bfc338a7a0ce43bce7b114382d42bcbf87e4c2b775ebe8ff0ca124def350760e2a1
|
7
|
+
data.tar.gz: 8064e69102f3ab07d38779f64ce0483276f20e06234ebfbf1bd949af37a572449d1778d7e7b3a3374d4a72cd908c225e7b8d58df219f38bcc0834b68de419f00
|
data/bin/vast-sd
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
|
5
|
+
require "dry/cli"
|
6
|
+
|
7
|
+
require_relative "../lib/vast-sd/start"
|
8
|
+
require_relative "../lib/vast-sd/config"
|
9
|
+
|
10
|
+
DEFAULT_CONFIG_FILE_PATH = File.join(Dir.home, '.vast-sd.conf.json')
|
11
|
+
|
12
|
+
def load_config(path)
|
13
|
+
if File.exists?(path)
|
14
|
+
JSON.parse(File.read path)
|
15
|
+
else
|
16
|
+
{}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module VastSd
|
21
|
+
module Commands
|
22
|
+
extend Dry::CLI::Registry
|
23
|
+
|
24
|
+
class Config < Dry::CLI::Command
|
25
|
+
desc "Run configuration wizard"
|
26
|
+
|
27
|
+
option :c, desc: "Configuration file path", default: DEFAULT_CONFIG_FILE_PATH
|
28
|
+
|
29
|
+
def call(c:, **)
|
30
|
+
VastSd::Config.new(path: c, config: load_config(c)).run
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Start < Dry::CLI::Command
|
35
|
+
desc "Start instance on vast.ai"
|
36
|
+
|
37
|
+
option :c, desc: "Configuration file path", default: DEFAULT_CONFIG_FILE_PATH
|
38
|
+
|
39
|
+
def call(c:, **)
|
40
|
+
VastSd::Start.new(config: load_config(c)).run
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
register "config", Config
|
45
|
+
register "start", Start
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
Dry::CLI.new(VastSd::Commands).call
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
require 'tty-logger'
|
4
|
+
require 'tty-prompt'
|
5
|
+
|
6
|
+
require_relative './vast'
|
7
|
+
|
8
|
+
module VastSd
|
9
|
+
class Config
|
10
|
+
include Vast
|
11
|
+
|
12
|
+
def initialize(path: ,config:)
|
13
|
+
@path = path
|
14
|
+
@config = config
|
15
|
+
|
16
|
+
@logger = TTY::Logger.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
preferred_gpus = pick_preferred_gpus
|
21
|
+
models = pick_models
|
22
|
+
|
23
|
+
if File.exists?(@path) && !ask_boolean("Do you want to overwrite your current configuration file? (y/n)")
|
24
|
+
return
|
25
|
+
end
|
26
|
+
|
27
|
+
File.write(@path, JSON.pretty_generate({
|
28
|
+
preferred_gpus: preferred_gpus,
|
29
|
+
models: models
|
30
|
+
}))
|
31
|
+
|
32
|
+
@logger.success "Configuration saved at:", @path
|
33
|
+
end
|
34
|
+
|
35
|
+
def pick_preferred_gpus
|
36
|
+
prompt = TTY::Prompt.new
|
37
|
+
|
38
|
+
offers = vast_cmd("search", "offers")
|
39
|
+
|
40
|
+
gpus_selection = offers.map do |offer|
|
41
|
+
offer["gpu_name"]
|
42
|
+
end.uniq
|
43
|
+
|
44
|
+
prompt.multi_select("Select your preferred GPU types", gpus_selection.sort, default: "RTX 4090", per_page: 20)
|
45
|
+
end
|
46
|
+
|
47
|
+
def pick_models
|
48
|
+
|
49
|
+
{}.tap do |models|
|
50
|
+
loop do
|
51
|
+
if !ask_boolean("Do you want to add #{models.count == 0 ? "a" : "another"} model? (y/n)")
|
52
|
+
break
|
53
|
+
end
|
54
|
+
|
55
|
+
model_kind = pick_model_kind
|
56
|
+
|
57
|
+
uri = ask_uri("Public URI of the model:").to_s
|
58
|
+
|
59
|
+
models[model_kind] ||= []
|
60
|
+
models[model_kind].push(uri)
|
61
|
+
|
62
|
+
@logger.success "Model added:", File.basename(uri)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def pick_model_kind
|
68
|
+
prompt = TTY::Prompt.new
|
69
|
+
|
70
|
+
prompt.select("What kind of model do you want to add?", ["Stable-diffusion", "Lora"])
|
71
|
+
end
|
72
|
+
|
73
|
+
def ask_boolean(label)
|
74
|
+
prompt = TTY::Prompt.new
|
75
|
+
|
76
|
+
prompt.ask(label, convert: :boolean)
|
77
|
+
end
|
78
|
+
|
79
|
+
def ask_uri(label)
|
80
|
+
prompt = TTY::Prompt.new
|
81
|
+
|
82
|
+
prompt.ask(label, convert: :uri)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'json'
|
3
|
+
require 'tty-logger'
|
4
|
+
require 'tty-spinner'
|
5
|
+
require 'tty-prompt'
|
6
|
+
|
7
|
+
require_relative './vast'
|
8
|
+
|
9
|
+
module VastSd
|
10
|
+
class Start
|
11
|
+
include Vast
|
12
|
+
|
13
|
+
def initialize(config:)
|
14
|
+
@config = config
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
logger = TTY::Logger.new
|
19
|
+
prompt = TTY::Prompt.new
|
20
|
+
|
21
|
+
user_info = vast_cmd("show", "user")
|
22
|
+
logger.info "Credit balance", sprintf("$%2.2f", user_info["credit"])
|
23
|
+
|
24
|
+
offers = vast_cmd("search", "offers")
|
25
|
+
|
26
|
+
offers = offers.select do |offer|
|
27
|
+
has_good_gpu =
|
28
|
+
if @config["preferred_gpus"].to_a.count > 0
|
29
|
+
@config["preferred_gpus"].include?(offer["gpu_name"])
|
30
|
+
else
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
has_good_gpu && offer["rentable"]
|
35
|
+
end.sort_by do |offer|
|
36
|
+
offer["dph_total"]
|
37
|
+
end
|
38
|
+
|
39
|
+
id = prompt.select("Instance selection", offers.map { |offer|
|
40
|
+
{
|
41
|
+
name: sprintf("%-40s %s", "#{offer['num_gpus']} #{offer["gpu_name"]} - #{offer["geolocation"]}", sprintf("$%2.2f/hr", offer["dph_total"])),
|
42
|
+
value: offer["id"]
|
43
|
+
}
|
44
|
+
}, per_page: 20)
|
45
|
+
|
46
|
+
contract = vast_cmd(
|
47
|
+
"create",
|
48
|
+
"instance", id,
|
49
|
+
"--disk", 20,
|
50
|
+
"--jupyter",
|
51
|
+
"--jupyter-dir", "/",
|
52
|
+
"--direct",
|
53
|
+
"--env", "-e JUPYTER_DIR=/ -p 3000:3000",
|
54
|
+
"--onstart-cmd", %{
|
55
|
+
sed -i '/rsync -au --remove-source-files \/venv\/ \/workspace\/venv\//a source \/workspace\/venv\/bin\/activate\n pip install jupyter_core' /start.sh;
|
56
|
+
/start.sh
|
57
|
+
},
|
58
|
+
"--image", "runpod/stable-diffusion:web-automatic-8.0.3"
|
59
|
+
)
|
60
|
+
|
61
|
+
contract_id = contract["new_contract"]
|
62
|
+
|
63
|
+
at_exit do
|
64
|
+
vast_cmd("destroy", "instance", contract_id)
|
65
|
+
|
66
|
+
logger.success "Instance destroyed"
|
67
|
+
end
|
68
|
+
|
69
|
+
logger.success "Contract ID", contract_id
|
70
|
+
|
71
|
+
# Wait until instance is ready
|
72
|
+
TTY::Spinner.new("[:spinner] Waiting for instance: :status", clear: true).run do |spinner|
|
73
|
+
loop do
|
74
|
+
instance = vast_cmd("show", "instance", contract_id)
|
75
|
+
|
76
|
+
spinner.update status: instance["actual_status"] || "Initialization"
|
77
|
+
|
78
|
+
if instance["actual_status"] == "running"
|
79
|
+
port = instance["ports"]["3000/tcp"][0]["HostPort"]
|
80
|
+
|
81
|
+
logger.info "http://#{instance["public_ipaddr"]}:#{port}"
|
82
|
+
|
83
|
+
break
|
84
|
+
else
|
85
|
+
sleep 5
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
ssh_info = vast_cmd('ssh-url', contract_id)
|
91
|
+
logger.info ssh_info
|
92
|
+
|
93
|
+
@config["models"].entries.each do |namespace, urls|
|
94
|
+
urls.each do |url|
|
95
|
+
TTY::Spinner.new("[:spinner] Loading \"#{File.basename(url)}\" in models/#{namespace}", clear: true).run do
|
96
|
+
upload_cmd = "curl #{Shellwords.escape(url)} -o /workspace/stable-diffusion-webui/models/#{namespace}/#{File.basename(url)}"
|
97
|
+
|
98
|
+
loop do
|
99
|
+
system("ssh -o StrictHostKeyChecking=no -q #{Shellwords.escape(ssh_info.chomp)} #{Shellwords.escape(upload_cmd)} &> /dev/null")
|
100
|
+
break if $? == 0
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
logger.success "Instance ready"
|
107
|
+
|
108
|
+
# Run watchdog
|
109
|
+
spinner = TTY::Spinner.new("[:spinner] Running: :balance")
|
110
|
+
spinner.auto_spin
|
111
|
+
|
112
|
+
loop do
|
113
|
+
begin
|
114
|
+
user_info = vast_cmd("show", "user")
|
115
|
+
spinner.update balance: "credit balance #{sprintf("$%2.2f", user_info["credit"])}"
|
116
|
+
|
117
|
+
instance = vast_cmd("show", "instance", contract_id)
|
118
|
+
|
119
|
+
if instance["actual_status"] != "running"
|
120
|
+
logger.error "Instance #{instance['actual_status']}"
|
121
|
+
exit
|
122
|
+
else
|
123
|
+
sleep 60
|
124
|
+
end
|
125
|
+
rescue Interrupt
|
126
|
+
exit(0)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
spinner.stop
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/vast-sd/vast.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'shellwords'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require 'tty-spinner'
|
5
|
+
require 'tty-command'
|
6
|
+
|
7
|
+
module VastSd
|
8
|
+
module Vast
|
9
|
+
def vast_cmd(*params)
|
10
|
+
spinner = TTY::Spinner.new(clear: true)
|
11
|
+
spinner.auto_spin
|
12
|
+
|
13
|
+
cmd = TTY::Command.new(printer: :null)
|
14
|
+
|
15
|
+
params = params.map do |param|
|
16
|
+
Shellwords.escape(param)
|
17
|
+
end
|
18
|
+
|
19
|
+
output, error = cmd.run("vast #{params.join(" ")} --raw 2> /dev/null")
|
20
|
+
|
21
|
+
# Cleanup output
|
22
|
+
output = output.gsub(/.+HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHh/m, '')
|
23
|
+
|
24
|
+
spinner.stop
|
25
|
+
|
26
|
+
begin
|
27
|
+
JSON.parse(output)
|
28
|
+
rescue JSON::ParserError
|
29
|
+
output
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: vast-sd-cli
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Anonymous
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-08-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dry-cli
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 1.0.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 1.0.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: tty-logger
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.6.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.6.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: tty-spinner
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 0.9.3
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 0.9.3
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: tty-prompt
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.23.1
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.23.1
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tty-command
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.10.1
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.10.1
|
83
|
+
description:
|
84
|
+
email:
|
85
|
+
- anonymous@unknown.com
|
86
|
+
executables:
|
87
|
+
- vast-sd
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- bin/vast-sd
|
92
|
+
- lib/vast-sd/config.rb
|
93
|
+
- lib/vast-sd/start.rb
|
94
|
+
- lib/vast-sd/vast.rb
|
95
|
+
- lib/vast-sd/version.rb
|
96
|
+
homepage:
|
97
|
+
licenses:
|
98
|
+
- MIT
|
99
|
+
metadata: {}
|
100
|
+
post_install_message:
|
101
|
+
rdoc_options: []
|
102
|
+
require_paths:
|
103
|
+
- lib
|
104
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - ">="
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: 2.6.0
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
version: '0'
|
114
|
+
requirements: []
|
115
|
+
rubygems_version: 3.3.3
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: Little CLI tool to run and provision Stable Diffusion on Vast.ai (WIP)
|
119
|
+
test_files: []
|