From e5cbf7f0ecc64c1d9fb5e239ec9c92a9380b65e0 Mon Sep 17 00:00:00 2001 From: Brice Videau Date: Wed, 2 Oct 2024 15:37:38 -0500 Subject: [PATCH] Implement xmlrpc tuner server. --- .github/workflows/presubmit.yml | 10 +- bindings/ruby/Makefile.am | 1 + bindings/ruby/test/test_tuner.rb | 125 +++++++++++++++++ bindings/ruby/test/tuner_server.rb | 211 +++++++++++++++++++++++++++++ 4 files changed, 342 insertions(+), 5 deletions(-) create mode 100644 bindings/ruby/test/tuner_server.rb diff --git a/.github/workflows/presubmit.yml b/.github/workflows/presubmit.yml index 61376b39..1dc4a003 100644 --- a/.github/workflows/presubmit.yml +++ b/.github/workflows/presubmit.yml @@ -30,7 +30,7 @@ jobs: compiler: g++ steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: sudo apt update; sudo apt install -y ${{ matrix.compiler }} ruby-dev libgsl-dev python3-dev valgrind if: ${{ matrix.os == 'ubuntu-latest' }} - run: brew install gsl automake libtool @@ -39,7 +39,7 @@ jobs: with: python-version: '3.11' if: ${{ matrix.os == 'macos-latest' }} - - run: gem install --user-install rake ffi ffi-value whittle + - run: gem install --user-install rake ffi ffi-value whittle xmlrpc - run: pip3 install --user parglare - run: ./autogen.sh - run: mkdir -p build @@ -55,7 +55,7 @@ jobs: - run: make -j check-valgrind-helgrind working-directory: build if: ${{ matrix.os == 'ubuntu-latest' }} - - uses: actions/upload-artifact@v2 + - uses: actions/upload-artifact@v4 if: failure() with: name: build-and-check @@ -82,7 +82,7 @@ jobs: with: python-version: '3.11' if: ${{ matrix.os == 'macos-latest' }} - - run: gem install --user-install rake ffi ffi-value whittle + - run: gem install --user-install rake ffi ffi-value whittle xmlrpc - run: pip3 install --user parglare - run: ./autogen.sh - run: mkdir -p build @@ -110,7 +110,7 @@ jobs: with: python-version: '3.11' if: ${{ matrix.os == 'macos-latest' }} - - run: gem install --user-install rake ffi ffi-value whittle + - run: gem install --user-install rake ffi ffi-value whittle xmlrpc - run: pip3 install --user parglare - run: ./autogen.sh - run: mkdir -p build diff --git a/bindings/ruby/Makefile.am b/bindings/ruby/Makefile.am index 79586395..db69d249 100644 --- a/bindings/ruby/Makefile.am +++ b/bindings/ruby/Makefile.am @@ -40,6 +40,7 @@ EXTRA_DIST = \ test/test_configuration_space.rb \ test/test_distribution.rb \ test/test_evaluation.rb \ + test/tuner_server.rb \ rakefile \ cconfigspace.gemspec \ LICENSE diff --git a/bindings/ruby/test/test_tuner.rb b/bindings/ruby/test/test_tuner.rb index 961cfbb3..c9435e9c 100644 --- a/bindings/ruby/test/test_tuner.rb +++ b/bindings/ruby/test/test_tuner.rb @@ -170,5 +170,130 @@ def test_user_defined assert( t_copy.optima.collect(&:configuration).include?(t_copy.suggest) ) File.delete('tuner.ccs') end + + require 'open3' + require 'xmlrpc/client' + require 'base64' + class TunerProxy + attr_reader :server + attr_reader :id + attr_reader :handle_map + attr_reader :objective_space + def initialize(name: "", objective_space: nil) + @server = XMLRPC::Client.new2('http://localhost:8080/RPC2') + connected = false + start = Time.now + while !connected + begin + connected = server.call('connected') + rescue + raise if Time.now - start > 10 + end + end + if objective_space + @objective_space = objective_space + buff = objective_space.serialize + str = Base64.encode64(buff.get_bytes(0, buff.size)) + @id, result = server.call('tuner.create', name, str) + @handle_map = CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(result))) + else + @id, result = server.call('tuner.load', name) + @handle_map = CCS::Map.new + @objective_space = CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(result)), handle_map: @handle_map, map_handles: true) + map = CCS::Map.new + @handle_map.pairs.select { |_ , v| + v.is_a?(CCS::Context) || v.is_a?(CCS::TreeSpace) + }.each { |k, v| + map[CCS::Object::new(v.handle, retain: false, auto_release: false)] = k + } + buff = map.serialize + server.call('tuner.set_handle_map', @id, Base64.encode64(buff.get_bytes(0, buff.size))) + end + end + + def ask(count = 1) + server.call('tuner.ask', @id, 100).collect { |c| + CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(c)), handle_map: @handle_map) + } + end + + def tell(evals = nil) + evals_serialized = evals.collect { |e| e.serialize }.collect { |buff| Base64.encode64(buff.get_bytes(0, buff.size)) } + server.call('tuner.tell', @id, evals_serialized) + self + end + + def history + server.call('tuner.history', @id).collect { |e| + CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(e)), handle_map: @handle_map) + } + end + + def history_size + server.call('tuner.history_size', @id) + end + + def optima + server.call('tuner.optima', @id).collect { |e| + CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(e)), handle_map: @handle_map) + } + end + + def num_optima + server.call('tuner.num_optima', @id) + end + + def suggest + e = server.call('tuner.suggest', @id) + CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(e)), handle_map: @handle_map) + end + + def save + server.call('tuner.save', @id) + end + end + + def test_server + begin + pid = nil + thr = Thread.new do + Open3.popen2e(RbConfig.ruby, File.join(File.dirname(__FILE__), 'tuner_server.rb')) { |stdin, stdout_stderr, wait_thr| + pid = wait_thr.pid + stdout_stderr.read + } + end + os = create_tuning_problem + t = TunerProxy.new(name: "my_tuner", objective_space: os) + func = lambda { |(x, y, z)| + [(x-2)**2, Math.sin(z+y)] + } + evals = t.ask(100).collect { |c| + CCS::Evaluation::new(objective_space: os, configuration: c, values: func[c.values]) + } + t.tell evals + hist = t.history + assert_equal(100, hist.size) + evals = t.ask(100).collect { |c| + CCS::Evaluation::new(objective_space: os, configuration: c, values: func[c.values]) + } + t.tell evals + assert_equal(200, t.history_size) + objs = t.optima.collect(&:objective_values).sort + objs.collect { |(_, v)| v }.each_cons(2) { |v1, v2| assert( (v1 <=> v2) > 0 ) } + assert( t.optima.collect(&:configuration).include?(t.suggest) ) + + t.save + t_copy = TunerProxy.new(name: "my_tuner") + hist = t_copy.history + assert_equal(200, hist.size) + assert_equal(t.num_optima, t_copy.num_optima) + objs = t_copy.optima.collect(&:objective_values).sort + objs.collect { |(_, v)| v }.each_cons(2) { |v1, v2| assert( (v1 <=> v2) > 0 ) } + assert( t_copy.optima.collect(&:configuration).include?(t_copy.suggest) ) + ensure + Process.kill("TERM", pid) + thr.join + end + end end diff --git a/bindings/ruby/test/tuner_server.rb b/bindings/ruby/test/tuner_server.rb new file mode 100644 index 00000000..15b05cf3 --- /dev/null +++ b/bindings/ruby/test/tuner_server.rb @@ -0,0 +1,211 @@ +require 'minitest/autorun' +require_relative '../lib/cconfigspace' +require 'xmlrpc/server' +require 'base64' + +CCS.init + +class TunerData + attr_accessor :history, :optima + def initialize + @history = [] + @optima = [] + end +end + +del = lambda { |tuner| nil } +ask = lambda { |tuner, _, count| + if count + cs = tuner.search_space + [cs.samples(count), count] + else + [nil, 1] + end +} +tell = lambda { |tuner, evaluations| + tuner.tuner_data.history.concat(evaluations) + evaluations.each { |e| + discard = false + tuner.tuner_data.optima = tuner.tuner_data.optima.collect { |o| + unless discard + case e.compare(o) + when :CCS_COMPARISON_EQUIVALENT, :CCS_COMPARISON_WORSE + discard = true + o + when :CCS_COMPARISON_NOT_COMPARABLE + o + else + nil + end + else + o + end + }.compact + tuner.tuner_data.optima.push(e) unless discard + } +} +get_history = lambda { |tuner, _| + tuner.tuner_data.history +} +get_optima = lambda { |tuner, _| + tuner.tuner_data.optima +} +suggest = lambda { |tuner, _| + if tuner.tuner_data.optima.empty? + ask.call(tuner, 1) + else + tuner.tuner_data.optima.sample.configuration + end +} +get_vector_data = lambda { |otype, name| + [CCS::UserDefinedTuner.get_vector(del: del, ask: ask, tell: tell, get_optima: get_optima, get_history: get_history, suggest: suggest), TunerData.new] +} + +s = XMLRPC::Server.new(8080) +count = 0 +tuners = {} +# Could be on disk +tuners_store = {} +mutex = Mutex.new + +TunerStruct = Struct.new(:tuner, :handle_map) + +s.add_handler('connected') do + true +end + +s.add_handler('tuner.create') do |name, os_string| + handle_map = CCS::Map.new + os_string = Base64.decode64(os_string) + os = CCS.deserialize(buffer: FFI::MemoryPointer.from_string(os_string), handle_map: handle_map, map_handles: true) + t = CCS::UserDefinedTuner::new(name: name, objective_space: os, del: del, ask: ask, tell: tell, get_optima: get_optima, get_history: get_history, suggest: suggest, tuner_data: TunerData.new) + + map = CCS::Map.new + handle_map.pairs.select { |_ , v| + v.is_a?(CCS::Context) || v.is_a?(CCS::TreeSpace) + }.each { |k, v| + map[CCS::Object::new(v.handle, retain: false, auto_release: false)] = k + } + buff = map.serialize + tstruct = TunerStruct.new(t, handle_map) + id = nil + mutex.synchronize { + id = count + count += 1 + tuners[id] = tstruct + } + [id, Base64.encode64(buff.get_bytes(0, buff.size))] +end + +s.add_handler('tuner.ask') do |id, count| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + tstruct.tuner.ask(count).collect { |c| + buff = c.serialize + Base64.encode64(buff.get_bytes(0, buff.size)) + } +end + +s.add_handler('tuner.tell') do |id, evals| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + evals.collect! { |e| + e = Base64.decode64(e) + CCS.deserialize(buffer: FFI::MemoryPointer.from_string(e), handle_map: tstruct.handle_map) + } + tstruct.tuner.tell evals + true +end + +s.add_handler('tuner.history') do |id| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + tstruct.tuner.history.collect { |e| + buff = e.serialize + Base64.encode64(buff.get_bytes(0, buff.size)) + } +end + +s.add_handler('tuner.history_size') do |id| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + tstruct.tuner.history_size +end + +s.add_handler('tuner.optima') do |id| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + tstruct.tuner.optima.collect { |e| + buff = e.serialize + Base64.encode64(buff.get_bytes(0, buff.size)) + } +end + +s.add_handler('tuner.num_optima') do |id| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + tstruct.tuner.num_optima +end + +s.add_handler('tuner.suggest') do |id| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + e = tstruct.tuner.suggest + buff = e.serialize + Base64.encode64(buff.get_bytes(0, buff.size)) +end + +s.add_handler('tuner.save') do |id| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + buff = tstruct.tuner.serialize + mutex.synchronize { + tuners_store[tstruct.tuner.name] = buff.get_bytes(0, buff.size) + } + true +end + +s.add_handler('tuner.load') do |name| + buff = nil + mutex.synchronize { + buff = tuners_store[name] + } + t = CCS.deserialize(buffer: buff, vector_callback: get_vector_data) + tstruct = TunerStruct.new(t, nil) + id = nil + mutex.synchronize { + id = count + count += 1 + tuners[id] = tstruct + } + buff = t.objective_space.serialize + [id, Base64.encode64(buff.get_bytes(0, buff.size))] +end + +s.add_handler('tuner.set_handle_map') do |id, map_str| + tstruct = nil + mutex.synchronize { + tstruct = tuners[id] + } + tstruct.handle_map = CCS.deserialize(buffer: FFI::MemoryPointer.from_string(Base64.decode64(map_str))) + true +end + +s.serve +