From 0b9cd40f79f543a3845ba45cc3514fa47d33cd1c Mon Sep 17 00:00:00 2001 From: Paul Buetow Date: Mon, 25 May 2026 22:23:05 +0300 Subject: Fix Gemma 4 vLLM startup and improve partial-provisioning UX - provisioning.rb: auto-install pytest before pre_start_cmd to fix missing dependency in nightly vLLM images that crashes EngineCore on cupy import - cli.rb: relax active_config_loaders/vm1_alive? to match by vm_id+public_ip instead of requiring status==ACTIVE, so watch/status/test work on VMs that were interrupted mid-provisioning - vm_lifecycle.rb: persist API status/vm_state back to state file during status calls; add 'Provisioning: incomplete' notice when provisioned_at is missing - provisioning_orchestrator.rb: add wait_for_ssh polling before ensure_trusted_host to fix race between SSH daemon readiness and keyscan --- hypr.fish | 2 +- lib/hyperstack/cli.rb | 17 +++++++---------- lib/hyperstack/provisioning.rb | 3 +++ lib/hyperstack/provisioning_orchestrator.rb | 7 +++++++ lib/hyperstack/vm_lifecycle.rb | 7 +++++++ 5 files changed, 25 insertions(+), 11 deletions(-) diff --git a/hypr.fish b/hypr.fish index 78e1f7a..0fccad2 100644 --- a/hypr.fish +++ b/hypr.fish @@ -1,7 +1,7 @@ # Dual-VM setup (hyperstack-vm1/vm2.toml -> hyperstack1/2.wg1) abbr pi-hyperstack pi --model hyperstack1/Qwen/Qwen3.6-27B-FP8 abbr pi-hyperstack-coder pi --model hyperstack1/Qwen/Qwen3.6-27B-FP8 -abbr pi-hyperstack-qwen36 pi --model hyperstack2/Qwen/Qwen3.6-27B-FP8 +abbr pi-hyperstack-qwen36 pi --model hyperstack1/Qwen/Qwen3.6-27B-FP8 abbr pi-hyperstack-gemma4 pi --model hyperstack2/cyankiwi/gemma-4-31B-it-AWQ-4bit abbr hyperstack-create ruby ~/git/hypr/hyperstack.rb create diff --git a/lib/hyperstack/cli.rb b/lib/hyperstack/cli.rb index b5bcaff..b466105 100644 --- a/lib/hyperstack/cli.rb +++ b/lib/hyperstack/cli.rb @@ -110,30 +110,27 @@ module HyperstackVM end end - # Returns only the config loaders whose state files exist, i.e. VMs that have - # been provisioned at least once. Used by watch/status/test when the user - # wants to see whatever is currently up without specifying --vm explicitly. - # VMs that have an active (live) state file: state exists, has a public IP, - # and status is ACTIVE. Used by watch/status/test when falling back from - # a dead or unprovisioned default VM. + # Returns only the config loaders whose state files exist and have a tracked VM. + # Used by watch/status/test when the user wants to see whatever is currently + # up without specifying --vm explicitly. def active_config_loaders pair_config_loaders.filter_map do |loader| next unless File.exist?(loader.config.state_file) state = JSON.parse(File.read(loader.config.state_file)) - state['public_ip'] && state['status'] == 'ACTIVE' ? loader : nil + state['public_ip'] && state['vm_id'] ? loader : nil rescue JSON::ParserError, Errno::ENOENT nil end end - # True when VM1 has a state file that actually points to a running VM. + # True when VM1 has a state file with a tracked VM ID and public IP. def vm1_alive? path = ConfigLoader.load(vm_config_path('1')).config.state_file return false unless File.exist?(path) state = JSON.parse(File.read(path)) - state['public_ip'] && state['status'] == 'ACTIVE' + state['public_ip'] && state['vm_id'] rescue JSON::ParserError, Errno::ENOENT false end @@ -263,7 +260,7 @@ module HyperstackVM def run_status loaders = default_or_active_loaders if loaders.empty? - puts 'No active VMs found.' + puts 'No active VMs found. Run `create --vm 1|2|both` first.' puts puts '[local-wireguard]' build_manager(ConfigLoader.load(vm_config_path('1')).config).show_local_wireguard(nil) diff --git a/lib/hyperstack/provisioning.rb b/lib/hyperstack/provisioning.rb index 19a3d33..dfe5b8d 100644 --- a/lib/hyperstack/provisioning.rb +++ b/lib/hyperstack/provisioning.rb @@ -162,6 +162,9 @@ module HyperstackVM # When set, --entrypoint bash is used so the command can patch dependencies at runtime # (e.g. upgrading transformers for Gemma 4, which requires transformers>=5.x). pre_cmd = (cfg.key?('pre_start_cmd') ? cfg['pre_start_cmd'] : nil) || @config.vllm_pre_start_cmd + # vLLM nightly images may be missing pytest which cupy imports during engine init. + # Prepend a quiet install so any pre_start_cmd also satisfies this dependency. + pre_cmd = "pip install -q pytest 2>/dev/null; #{pre_cmd}" if pre_cmd port = @config.ollama_port docker_args = [ diff --git a/lib/hyperstack/provisioning_orchestrator.rb b/lib/hyperstack/provisioning_orchestrator.rb index 7dbb10e..b9f9e3d 100644 --- a/lib/hyperstack/provisioning_orchestrator.rb +++ b/lib/hyperstack/provisioning_orchestrator.rb @@ -29,6 +29,7 @@ module HyperstackVM state['security_rules'] = Array(vm['security_rules']).map { |r| normalize_rule(r) } @state_store.save(state) + wait_for_ssh(state['public_ip']) @ssh_runner.ensure_trusted_host(state['public_ip']) if @config.guest_bootstrap_enabled? && state['bootstrapped_at'].nil? @@ -78,6 +79,12 @@ module HyperstackVM state end + def wait_for_ssh(host) + with_polling("SSH on #{host}:#{@config.ssh_port} to become reachable", timeout: 300) do + @ssh_runner.tcp_open?(host, @config.ssh_port) + end + end + def wait_for_ready(vm_id) with_polling("VM #{vm_id} to become ready for firewall updates") do vm = @client.get_vm(vm_id) diff --git a/lib/hyperstack/vm_lifecycle.rb b/lib/hyperstack/vm_lifecycle.rb index cc52880..22fbe62 100644 --- a/lib/hyperstack/vm_lifecycle.rb +++ b/lib/hyperstack/vm_lifecycle.rb @@ -110,9 +110,16 @@ module HyperstackVM info "Tracked VM: #{state['vm_id']} #{vm['name']}" info "Status: #{vm['status']} / #{vm['vm_state']}" info "Public IP: #{connect_host_for(vm) || 'none'}" + unless state['provisioned_at'] + info "Provisioning: incomplete — run `create` to resume" + end info "Service mode: #{service_summary(vllm: vllm_e, ollama: ollama_e)}" info "Active model: #{state['vllm_model'] || @config.vllm_model}" if vllm_e info "Missing firewall rules: #{missing.empty? ? 'none' : missing.size}" + state['status'] = vm['status'] + state['vm_state'] = vm['vm_state'] + state['public_ip'] = connect_host_for(vm) || state['public_ip'] + @state_store.save(state) rescue Error => e warn_out "Unable to load VM #{state['vm_id']}: #{e.message}" return state&.dig('public_ip') -- cgit v1.2.3