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 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: []