lemans 0.2.1 → 0.2.3
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 +4 -4
- data/exe/lemans-remote +107 -36
- data/lib/lemans/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6ce349f204cd0dc573a18932210dd5298941fe097f24576df1e318b29ce7152a
|
|
4
|
+
data.tar.gz: c9113c9bc9dbc4cf1f92d422b3ff929b20420e769828f55585f2a3eead34aa93
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 89ecd01b291a2d75475f346e8e780d87e104139e0ca34f48dde192325ec2ed593920e0e63f8b41a26b9e13144052e6bfe841831bc5d40a4432a0cd98d3fc39fc
|
|
7
|
+
data.tar.gz: 84b2bceb8d5b7c60b0e6513bb05b801d38081f8f59c55e96338a2a47d660c0521a31d224c365e0b694581f4e600552e4e9e390f3ccd6130d7fc61c8a9897cce2
|
data/exe/lemans-remote
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
# exe/lemans-remote provision
|
|
14
14
|
#
|
|
15
15
|
# Fire-and-forget a bench run on remote Daytona sandboxes — one sandbox
|
|
16
|
-
# per task (--run-in-band for a single sandbox
|
|
17
|
-
#
|
|
16
|
+
# per task (--run-in-band for a single sandbox, --attempts N to repeat
|
|
17
|
+
# each task in N sandboxes); results are archived to the
|
|
18
|
+
# lemans-remote-runs volume. Add --sync to wait for a single sandbox
|
|
18
19
|
# and download into ./runs directly instead:
|
|
19
20
|
#
|
|
20
21
|
# exe/lemans-remote run --bench ../ai-evals --task hello-world
|
|
@@ -22,9 +23,11 @@
|
|
|
22
23
|
#
|
|
23
24
|
# Then watch, fetch, and clean up:
|
|
24
25
|
#
|
|
25
|
-
# exe/lemans-remote status [--history]
|
|
26
|
-
# exe/lemans-remote pull-runs [RUN_ID ...] [--all]
|
|
26
|
+
# exe/lemans-remote status [--history] [--running | --complete]
|
|
27
|
+
# exe/lemans-remote pull-runs [RUN_ID ...] [--all] [--dry-run]
|
|
27
28
|
# exe/lemans-remote clobber RUN_ID ... | --all
|
|
29
|
+
# exe/lemans-remote snapshots
|
|
30
|
+
# exe/lemans-remote deprovision [--name SNAPSHOT]
|
|
28
31
|
#
|
|
29
32
|
# Credentials come from the host ENV: DAYTONA_API_KEY (or DAYTONA_TOKEN),
|
|
30
33
|
# OPENROUTER_API_KEY, LEMANS_PROVIDER_ORDER; forward extras with --env KEY.
|
|
@@ -47,7 +50,7 @@ require "timeout"
|
|
|
47
50
|
require "tmpdir"
|
|
48
51
|
require "openssl"
|
|
49
52
|
|
|
50
|
-
module LemansRemote
|
|
53
|
+
module LemansRemote # :nodoc: all
|
|
51
54
|
LABEL = "lemans-remote"
|
|
52
55
|
RUN_ID_LABEL = "lemans-remote/run-id"
|
|
53
56
|
STATUS_LABEL = "lemans-remote/status"
|
|
@@ -111,7 +114,7 @@ module LemansRemote
|
|
|
111
114
|
name
|
|
112
115
|
end
|
|
113
116
|
|
|
114
|
-
def drop(target = name)
|
|
117
|
+
def drop(target = name) # rubocop:disable Naming/PredicateMethod
|
|
115
118
|
existing = find(target)
|
|
116
119
|
return false unless existing
|
|
117
120
|
|
|
@@ -348,7 +351,7 @@ module LemansRemote
|
|
|
348
351
|
end
|
|
349
352
|
|
|
350
353
|
def manifests(sandbox)
|
|
351
|
-
code = 'require "json"; puts Dir.glob("/vault/*/manifest.json")' \
|
|
354
|
+
code = 'require "json"; puts Dir.glob("/vault/*/manifest.json", File::FNM_DOTMATCH)' \
|
|
352
355
|
".filter_map { |f| JSON.parse(File.read(f)) rescue nil }.to_json"
|
|
353
356
|
response = sandbox.process.exec(command: "ruby -e #{Shellwords.escape(code)}", timeout: 120)
|
|
354
357
|
raise "could not read the vault manifests: #{response.result}" unless response.exit_code.zero?
|
|
@@ -357,6 +360,14 @@ module LemansRemote
|
|
|
357
360
|
body.empty? ? [] : JSON.parse(body)
|
|
358
361
|
end
|
|
359
362
|
|
|
363
|
+
def archive_size(sandbox, run_id)
|
|
364
|
+
remote = "/vault/#{run_id}/runs.tar.gz"
|
|
365
|
+
response = sandbox.process.exec(command: "du -h #{Shellwords.escape(remote)}", timeout: 60)
|
|
366
|
+
return nil unless response.exit_code.zero?
|
|
367
|
+
|
|
368
|
+
response.result.to_s.split.first
|
|
369
|
+
end
|
|
370
|
+
|
|
360
371
|
def download_runs(sandbox, run_id, runs_dir)
|
|
361
372
|
remote = "/vault/#{run_id}/runs.tar.gz"
|
|
362
373
|
probe = sandbox.process.exec(command: "test -f #{Shellwords.escape(remote)}", timeout: 60)
|
|
@@ -384,7 +395,7 @@ module LemansRemote
|
|
|
384
395
|
def mark_pulled(sandbox, run_id)
|
|
385
396
|
code = 'require "json"; require "time"; ' \
|
|
386
397
|
'path = File.join("/vault", ARGV[0], "manifest.json"); ' \
|
|
387
|
-
|
|
398
|
+
"data = JSON.parse(File.read(path)); " \
|
|
388
399
|
'data["pulled_at"] = Time.now.utc.iso8601; ' \
|
|
389
400
|
"File.write(path, JSON.pretty_generate(data))"
|
|
390
401
|
sandbox.process.exec(command: "ruby -e #{Shellwords.escape(code)} #{Shellwords.escape(run_id)}", timeout: 60)
|
|
@@ -537,7 +548,7 @@ module LemansRemote
|
|
|
537
548
|
def run_metadata(sandbox)
|
|
538
549
|
{
|
|
539
550
|
"run_id" => @run_id,
|
|
540
|
-
"bench" => @bench.root.basename.to_s,
|
|
551
|
+
"bench" => @bench.root.expand_path.basename.to_s,
|
|
541
552
|
"tasks" => @tasks,
|
|
542
553
|
"models" => @models,
|
|
543
554
|
"args" => @extra_args,
|
|
@@ -722,6 +733,7 @@ module LemansRemote
|
|
|
722
733
|
option :sync, type: :boolean, default: false, desc: "Wait for the run and download the results directly"
|
|
723
734
|
option :run_in_band, type: :boolean, default: false, aliases: %w[--runInBand],
|
|
724
735
|
desc: "Async mode: run all tasks in one sandbox instead of one sandbox per task"
|
|
736
|
+
option :attempts, type: :numeric, default: 1, desc: "Async mode: repeat each task batch N times, one sandbox per attempt"
|
|
725
737
|
option :keep, type: :boolean, default: false, desc: "Sync mode: leave the sandbox around for debugging"
|
|
726
738
|
option :runs_dir, default: "runs", desc: "Sync mode: local directory to sync the results into"
|
|
727
739
|
option :env, repeatable: true, desc: "Forward an extra host ENV variable by name"
|
|
@@ -732,23 +744,28 @@ module LemansRemote
|
|
|
732
744
|
raise Thor::Error, "lemans-remote: no such task(s): #{unknown.join(", ")}" if unknown.any?
|
|
733
745
|
|
|
734
746
|
provisioner = build_provisioner
|
|
735
|
-
unless provisioner.provisioned?
|
|
736
|
-
|
|
737
|
-
|
|
747
|
+
raise Thor::Error, "lemans-remote: snapshot #{provisioner.name} not found — run `lemans-remote provision` first" unless provisioner.provisioned?
|
|
748
|
+
|
|
749
|
+
attempts = options[:attempts].to_i
|
|
750
|
+
raise Thor::Error, "lemans-remote: --attempts must be at least 1" if attempts < 1
|
|
751
|
+
raise Thor::Error, "lemans-remote: --attempts needs async mode — drop --sync" if options[:sync] && attempts > 1
|
|
738
752
|
|
|
739
753
|
models = options[:model] || []
|
|
740
754
|
batches = plan_batches(bench, tasks)
|
|
755
|
+
jobs = batches.flat_map { |batch| (1..attempts).map { [batch, attempts > 1 ? _1 : nil] } }
|
|
741
756
|
Vault.ensure unless options[:sync]
|
|
742
757
|
|
|
743
|
-
if
|
|
744
|
-
|
|
758
|
+
if jobs.size == 1
|
|
759
|
+
batch, attempt = jobs.first
|
|
760
|
+
exit_code = launch_batch(bench, batch, models, provisioner, attempt:)
|
|
745
761
|
say_status :detached, "`lemans-remote status` to watch, `lemans-remote pull-runs` to fetch results" unless options[:sync]
|
|
746
762
|
exit exit_code unless exit_code.zero?
|
|
747
763
|
return
|
|
748
764
|
end
|
|
749
765
|
|
|
750
|
-
|
|
751
|
-
|
|
766
|
+
fanout = attempts > 1 ? "#{batches.size} task batch(es) × #{attempts} attempts" : "one per task"
|
|
767
|
+
say_status :fanout, "#{jobs.size} sandboxes, #{fanout}"
|
|
768
|
+
failures = launch_batches(bench, jobs, models, provisioner)
|
|
752
769
|
say_status :detached, "`lemans-remote status` to watch, `lemans-remote pull-runs` to fetch results"
|
|
753
770
|
raise Thor::Error, "lemans-remote: failed to launch: #{failures.join("; ")}" if failures.any?
|
|
754
771
|
rescue Lemans::ConfigError, RuntimeError => e
|
|
@@ -757,6 +774,8 @@ module LemansRemote
|
|
|
757
774
|
|
|
758
775
|
desc "status", "List lemans-remote runs (--history adds completed runs whose sandboxes are gone)"
|
|
759
776
|
option :history, type: :boolean, default: false, desc: "Also read the vault manifests (spins a short-lived helper sandbox)"
|
|
777
|
+
option :running, type: :boolean, default: false, desc: "Only show runs still in flight"
|
|
778
|
+
option :complete, type: :boolean, default: false, desc: "Only show finished runs (success, failed, or stale)"
|
|
760
779
|
def status
|
|
761
780
|
rows = Fleet.rows.map { [_1.run_id, _1.status, _1.started_at || "?", _1.state, _1.sandbox.id] }
|
|
762
781
|
if options[:history]
|
|
@@ -768,8 +787,15 @@ module LemansRemote
|
|
|
768
787
|
end
|
|
769
788
|
end
|
|
770
789
|
|
|
790
|
+
filtered = options[:running] ^ options[:complete]
|
|
791
|
+
rows.select! { (_1[1] == "running") == options[:running] } if filtered
|
|
792
|
+
|
|
771
793
|
if rows.empty?
|
|
772
|
-
|
|
794
|
+
if filtered
|
|
795
|
+
say "no #{options[:running] ? "running" : "complete"} lemans-remote runs"
|
|
796
|
+
else
|
|
797
|
+
say "no lemans-remote runs#{" — try --history for completed ones" unless options[:history]}"
|
|
798
|
+
end
|
|
773
799
|
return
|
|
774
800
|
end
|
|
775
801
|
|
|
@@ -783,6 +809,7 @@ module LemansRemote
|
|
|
783
809
|
map "pull-runs" => :pull_runs
|
|
784
810
|
desc "pull-runs [RUN_IDS...]", "Download archived runs from the vault into the local runs directory"
|
|
785
811
|
option :all, type: :boolean, default: false, desc: "Pull every completed run, even ones already pulled"
|
|
812
|
+
option :dry_run, type: :boolean, default: false, desc: "Preview what would be downloaded without pulling anything"
|
|
786
813
|
option :runs_dir, default: "runs", desc: "Local directory to sync the results into"
|
|
787
814
|
def pull_runs(*run_ids)
|
|
788
815
|
pulled = []
|
|
@@ -803,6 +830,16 @@ module LemansRemote
|
|
|
803
830
|
say_status :pull, "nothing new to pull (use --all to re-pull)", :yellow if targets.empty?
|
|
804
831
|
|
|
805
832
|
targets.each do |run_id|
|
|
833
|
+
if options[:dry_run]
|
|
834
|
+
size = vault.archive_size(sandbox, run_id)
|
|
835
|
+
if size
|
|
836
|
+
say_status :would_pull, "#{run_id} (#{size})", :cyan
|
|
837
|
+
else
|
|
838
|
+
say_status :missing, "#{run_id} has no runs.tar.gz (still running?)", :yellow
|
|
839
|
+
end
|
|
840
|
+
next
|
|
841
|
+
end
|
|
842
|
+
|
|
806
843
|
results = vault.download_runs(sandbox, run_id, options[:runs_dir])
|
|
807
844
|
if results
|
|
808
845
|
vault.mark_pulled(sandbox, run_id)
|
|
@@ -828,9 +865,7 @@ module LemansRemote
|
|
|
828
865
|
|
|
829
866
|
live = Fleet.live
|
|
830
867
|
if options[:all]
|
|
831
|
-
unless live.empty?
|
|
832
|
-
raise Thor::Error, "lemans-remote: #{live.size} run(s) still live (#{live.map(&:run_id).join(", ")}) — wait for them to finish"
|
|
833
|
-
end
|
|
868
|
+
raise Thor::Error, "lemans-remote: #{live.size} run(s) still live (#{live.map(&:run_id).join(", ")}) — wait for them to finish" unless live.empty?
|
|
834
869
|
return unless options[:force] || yes?("Delete the whole #{Vault::NAME} volume and every stopped run sandbox? [y/N]")
|
|
835
870
|
|
|
836
871
|
Fleet.rows.each { safe_delete_sandbox(_1.sandbox) }
|
|
@@ -858,6 +893,28 @@ module LemansRemote
|
|
|
858
893
|
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
859
894
|
end
|
|
860
895
|
|
|
896
|
+
desc "snapshots", "List lemans-remote orchestrator snapshots"
|
|
897
|
+
def snapshots
|
|
898
|
+
current = build_provisioner.name
|
|
899
|
+
rows = all_snapshots.select { _1.name.start_with?("#{LABEL}-") }.map do |snap|
|
|
900
|
+
shape = "#{snap.cpu.to_i}cpu/#{snap.mem.to_i}gb/#{snap.disk.to_i}gb"
|
|
901
|
+
[snap.name, snap.state, shape, snap.created_at.to_s[0, 19], snap.last_used_at.to_s[0, 19],
|
|
902
|
+
snap.name == current ? "current" : "-"]
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
if rows.empty?
|
|
906
|
+
say "no lemans-remote snapshots — run `lemans-remote provision` first"
|
|
907
|
+
return
|
|
908
|
+
end
|
|
909
|
+
|
|
910
|
+
rows.sort_by! { _1[3].to_s }
|
|
911
|
+
rows.reverse!
|
|
912
|
+
print_table([%w[snapshot state shape created last-used note]] + rows)
|
|
913
|
+
say "delete old ones with `lemans-remote deprovision --name SNAPSHOT`"
|
|
914
|
+
rescue Lemans::ConfigError, RuntimeError => e
|
|
915
|
+
raise Thor::Error, "lemans-remote: #{e.message}"
|
|
916
|
+
end
|
|
917
|
+
|
|
861
918
|
desc "deprovision", "Delete the orchestrator snapshot"
|
|
862
919
|
option :name, desc: "Delete a specific snapshot by name (default: the one for this shape and version)"
|
|
863
920
|
def deprovision
|
|
@@ -877,14 +934,14 @@ module LemansRemote
|
|
|
877
934
|
say Lemans::VERSION
|
|
878
935
|
end
|
|
879
936
|
|
|
937
|
+
FANOUT_THREADS = 4
|
|
938
|
+
|
|
880
939
|
private
|
|
881
940
|
|
|
882
941
|
def build_provisioner
|
|
883
942
|
Provisioner.new(cpus: options[:cpus].to_i, memory_gb: options[:memory].to_i, disk_gb: options[:disk].to_i)
|
|
884
943
|
end
|
|
885
944
|
|
|
886
|
-
FANOUT_THREADS = 4
|
|
887
|
-
|
|
888
945
|
def plan_batches(bench, tasks)
|
|
889
946
|
return [tasks] if options[:sync] || options[:run_in_band]
|
|
890
947
|
|
|
@@ -892,11 +949,11 @@ module LemansRemote
|
|
|
892
949
|
expanded.map { [_1] }
|
|
893
950
|
end
|
|
894
951
|
|
|
895
|
-
def launch_batch(bench, tasks, models, provisioner)
|
|
952
|
+
def launch_batch(bench, tasks, models, provisioner, attempt: nil)
|
|
896
953
|
Runner.new(
|
|
897
954
|
bench: bench,
|
|
898
955
|
snapshot: provisioner.name,
|
|
899
|
-
run_id: build_run_id(bench, tasks, models),
|
|
956
|
+
run_id: build_run_id(bench, tasks, models, attempt),
|
|
900
957
|
tasks: tasks,
|
|
901
958
|
models: models,
|
|
902
959
|
extra_args: options[:args].strip,
|
|
@@ -909,24 +966,26 @@ module LemansRemote
|
|
|
909
966
|
).call
|
|
910
967
|
end
|
|
911
968
|
|
|
912
|
-
def launch_batches(bench,
|
|
969
|
+
def launch_batches(bench, jobs, models, provisioner)
|
|
913
970
|
queue = Queue.new
|
|
914
|
-
|
|
971
|
+
jobs.each { queue << _1 }
|
|
915
972
|
failures = Queue.new
|
|
916
973
|
|
|
917
|
-
threads = [FANOUT_THREADS,
|
|
974
|
+
threads = [FANOUT_THREADS, jobs.size].min.times.map do
|
|
918
975
|
Thread.new do
|
|
919
976
|
loop do
|
|
920
|
-
batch =
|
|
977
|
+
batch, attempt =
|
|
921
978
|
begin
|
|
922
979
|
queue.pop(true)
|
|
923
980
|
rescue ThreadError
|
|
924
981
|
break
|
|
925
982
|
end
|
|
926
983
|
begin
|
|
927
|
-
launch_batch(bench, batch, models, provisioner)
|
|
984
|
+
launch_batch(bench, batch, models, provisioner, attempt:)
|
|
928
985
|
rescue StandardError => e
|
|
929
|
-
|
|
986
|
+
label = batch.empty? ? "all" : batch.join(",")
|
|
987
|
+
label += "/a#{attempt}" if attempt
|
|
988
|
+
failures << "#{label} (#{e.message})"
|
|
930
989
|
end
|
|
931
990
|
end
|
|
932
991
|
end
|
|
@@ -936,27 +995,39 @@ module LemansRemote
|
|
|
936
995
|
Array.new(failures.size) { failures.pop }
|
|
937
996
|
end
|
|
938
997
|
|
|
939
|
-
def build_run_id(bench, tasks, models)
|
|
940
|
-
parts = [bench.root.basename.to_s]
|
|
998
|
+
def build_run_id(bench, tasks, models, attempt = nil)
|
|
999
|
+
parts = [bench.root.expand_path.basename.to_s]
|
|
941
1000
|
parts << (tasks.size == 1 ? tasks.first : "#{tasks.size}tasks") if tasks.any?
|
|
942
1001
|
if models.any?
|
|
943
1002
|
short = models.first.split("/").last
|
|
944
1003
|
short += "+#{models.size - 1}" if models.size > 1
|
|
945
1004
|
parts << short
|
|
946
1005
|
end
|
|
1006
|
+
parts << "a#{attempt}" if attempt
|
|
947
1007
|
parts << (ENV["LEMANS_REMOTE_USER"] || ENV["USER"] || "anon")
|
|
948
1008
|
parts << Time.now.utc.strftime("%Y%m%dT%H%M%S")
|
|
949
1009
|
parts << SecureRandom.alphanumeric(4).downcase
|
|
950
1010
|
parts.map { name_part(_1) }.reject(&:empty?).join("-")
|
|
951
1011
|
end
|
|
952
1012
|
|
|
953
|
-
def name_part(value) = value.to_s.downcase.gsub(/[^a-z0-9._+]+/, "-").gsub(/\A
|
|
1013
|
+
def name_part(value) = value.to_s.downcase.gsub(/[^a-z0-9._+]+/, "-").gsub(/\A[-.]+|[-.]+\z/, "")
|
|
1014
|
+
|
|
1015
|
+
def all_snapshots
|
|
1016
|
+
items = []
|
|
1017
|
+
page = 1
|
|
1018
|
+
loop do
|
|
1019
|
+
result = LemansRemote.client.snapshot.list(page: page, limit: 200)
|
|
1020
|
+
items.concat(result.items)
|
|
1021
|
+
break if page >= result.total_pages.to_i
|
|
1022
|
+
|
|
1023
|
+
page += 1
|
|
1024
|
+
end
|
|
1025
|
+
items
|
|
1026
|
+
end
|
|
954
1027
|
|
|
955
1028
|
def with_vault(&block)
|
|
956
1029
|
provisioner = build_provisioner
|
|
957
|
-
unless provisioner.provisioned?
|
|
958
|
-
raise Thor::Error, "lemans-remote: snapshot #{provisioner.name} not found — run `lemans-remote provision` first"
|
|
959
|
-
end
|
|
1030
|
+
raise Thor::Error, "lemans-remote: snapshot #{provisioner.name} not found — run `lemans-remote provision` first" unless provisioner.provisioned?
|
|
960
1031
|
|
|
961
1032
|
vault = VaultClient.new(snapshot: provisioner.name)
|
|
962
1033
|
vault.with_helper { |sandbox| block.call(vault, sandbox) }
|
data/lib/lemans/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lemans
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Svyatoslav Kryukov
|
|
@@ -114,6 +114,7 @@ email:
|
|
|
114
114
|
- ardecvz@gmail.com
|
|
115
115
|
executables:
|
|
116
116
|
- lemans
|
|
117
|
+
- lemans-remote
|
|
117
118
|
extensions: []
|
|
118
119
|
extra_rdoc_files: []
|
|
119
120
|
files:
|