vast-sd-cli 0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -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
@@ -0,0 +1,3 @@
1
+ module VastSd
2
+ VERSION = '0.1'
3
+ 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: []