defmodule FLAMESlurmBackend do @moduledoc """ Slurm Backend implementation. ### Usage Configure the flame backend in our configuration or application setup: write a jobscript ``` #!/bin/bash #SBATCH -o flame.%j.out #SBATCH --nodes=1 #SBATCH --ntasks-per-node=1 #SBATCH --time=01:00:00 #SBATCH --mem=20G export SLURM_FLAME_HOST=$(ip -f inet addr show ib0 | awk '/inet/ {print $2}' | cut -d/ -f1) ``` setting a Partition with GPU instances and 1 GPU per job ``` #SBATCH --partition=gpu #SBATCH --gpus-per-node=1 ``` ``` # application.ex children = [ {FLAME.Pool, name: MyApp.SamplePool, code_sync: [ start_apps: true, sync_beams: Kino.beam_paths(), compress: false, extract_dir: { FLAMESlurmBackend.SlurmClient, :path_job_id, [Path.absname("extract_dir")<>"/"] } ], min: 0, max: 1, max_concurrency: 1, idle_shutdown_after: :timer.minutes(10), timeout: :infinity, boot_timeout: 360000, track_resources: true, backend: { FLAMESlurmBackend, slurm_job: } } ] ``` when running inside a Livebook: Start the Livebook with a matching Host part of the Erlang long name: Using LIVEBOOK_IP=0.0.0.0 is helpful if you create a portforwarding from the login Host of the Cluster. ``` #!/bin/bash export SLURM_FLAME_HOST=$(ip -f inet addr show ib0 | awk '/inet/ {print $2}' | cut -d/ -f1) epmd -daemon LIVEBOOK_IP=0.0.0.0 livebook server --name livebook@$SLURM_FLAME_HOST ``` ``` Kino.start_child( {FLAME.Pool, name: :runner, code_sync: [start_apps: true, sync_beams: Kino.beam_paths(), compress: false], min: 0, max: 1, max_concurrency: 10, idle_shutdown_after: :timer.minutes(1), timeout: :infinity, track_resources: true, backend: {FLAMESlurmBackend, slurm_job: }} ) ``` """ @behaviour FLAME.Backend alias FLAMESlurmBackend.SlurmClient require Logger defstruct parent_ref: nil, runner_node_name: nil, boot_timeout: nil, remote_terminator_pid: nil, log: false, slurm_job: nil @valid_opts ~w(slurm_job terminator_sup log boot_timeout)a @required_config ~w()a @impl true def init(opts) do conf = Application.get_env(:flame, __MODULE__) || [] [_node_base | _ip] = node() |> to_string() |> String.split("@") default = %FLAMESlurmBackend{ boot_timeout: 30_000, slurm_job: ~S""" #!/bin/bash #SBATCH -o flame.%j.out #SBATCH --nodes=1 #SBATCH --ntasks-per-node=1 #SBATCH --time=01:00:00 export SLURM_FLAME_HOST=$(ip -f inet addr show ib0 | awk '/inet/ {print $2}' | cut -d/ -f1) """ } provided_opts = conf |> Keyword.merge(opts) |> Keyword.validate!(@valid_opts) state = struct(default, provided_opts) for key <- @required_config do unless Map.get(state, key) do raise ArgumentError, "missing :#{key} config for #{inspect(__MODULE__)}" end end parent_ref = make_ref() new_state = struct(state, parent_ref: parent_ref ) {:ok, new_state} end @impl true def remote_spawn_monitor(%FLAMESlurmBackend{} = state, term) do case term do func when is_function(func, 0) -> {pid, ref} = Node.spawn_monitor(state.runner_node_name, func) {:ok, {pid, ref}} {mod, fun, args} when is_atom(mod) and is_atom(fun) and is_list(args) -> {pid, ref} = Node.spawn_monitor(state.runner_node_name, mod, fun, args) {:ok, {pid, ref}} other -> raise ArgumentError, "expected a null arity function or {mod, func, args}. Got: #{inspect(other)}" end end @impl true def system_shutdown() do System.stop() end @impl true def remote_boot(%FLAMESlurmBackend{parent_ref: parent_ref} = state) do log(state, "Remote Boot") {{new_state,job_id}, req_connect_time} = with_elapsed_ms(fn -> created_job = SlurmClient.start_job!(parent_ref, state.slurm_job, state.boot_timeout) case created_job do {:ok, job} -> log(state, "Job queued", job_id: job) {state, job} _ -> Logger.error("failed to schedule runner pod within #{state.boot_timeout}ms") exit(:timeout) end end) remaining_connect_window = state.boot_timeout - req_connect_time log(state, "Waiting for Remote UP.", remaining_connect_window: remaining_connect_window) remote_terminator_pid = receive do {^parent_ref, {:remote_up, remote_terminator_pid}} -> log(state, "Remote flame is Up!") remote_terminator_pid after remaining_connect_window -> Logger.error("failed to connect to runner job #{job_id} within #{state.boot_timeout}ms") SlurmClient.delete_job!(job_id) exit(:timeout) end new_state = struct!(new_state, remote_terminator_pid: remote_terminator_pid, runner_node_name: node(remote_terminator_pid) ) {:ok, remote_terminator_pid, new_state} end @impl true def handle_info(msg, state) do log(state, "Missed message: #{inspect(msg)}") {:noreply, state} end defp with_elapsed_ms(func) when is_function(func, 0) do {micro, result} = :timer.tc(func) {result, div(micro, 1000)} end defp log(state, msg, metadata \\ []) defp log(%FLAMESlurmBackend{log: false}, _, _), do: :ok defp log(%FLAMESlurmBackend{log: level}, msg, metadata) do Logger.log(level, msg, metadata) end end