Current section
Files
Jump to
Current section
Files
lib/crucible/harness/tinkex_runner.ex
defmodule Crucible.Harness.TinkexRunner do
@moduledoc """
Runs ML experiments using Tinkex backend.
This module implements the Runner behaviour for executing ML training experiments
through Tinkex. It manages training sessions, checkpointing, and result collection.
## Example
{:ok, experiment} = MLExperiment.new(name: "test", ...)
{:ok, runner} = TinkexRunner.init(experiment, [])
{:ok, runner} = TinkexRunner.run_stage(runner, :train, %{
type: :train,
config: %{epochs: 5, batch_size: 8}
})
results = TinkexRunner.get_results(runner)
"""
require Logger
alias Crucible.Harness.MLExperiment
@type t :: %__MODULE__{
experiment: MLExperiment.t(),
session: pid() | atom() | nil,
trainer: pid() | nil,
checkpoints: [String.t()],
results: map(),
status: :initialized | :running | :completed | :failed,
output_dir: String.t() | nil,
resume_from: String.t() | nil
}
defstruct [
:experiment,
:session,
:trainer,
:output_dir,
:resume_from,
checkpoints: [],
results: %{},
status: :initialized
]
@doc """
Initializes a runner with the given experiment configuration.
## Options
* `:session` - Training session pid
* `:output_dir` - Output directory for results
* `:resume_from` - Checkpoint name to resume from
## Examples
{:ok, runner} = TinkexRunner.init(experiment, session: my_session)
"""
@spec init(MLExperiment.t(), keyword()) :: {:ok, t()} | {:error, term()}
def init(%MLExperiment{} = experiment, opts) when is_list(opts) do
runner = %__MODULE__{
experiment: experiment,
session: Keyword.get(opts, :session),
output_dir: Keyword.get(opts, :output_dir),
resume_from: Keyword.get(opts, :resume_from),
checkpoints: [],
results: %{},
status: :initialized
}
{:ok, runner}
end
@doc """
Runs a specific stage of the experiment.
## Stage Types
* `:train` - Training stage using Tinkex
* `:eval` - Evaluation stage
* `:analysis` - Statistical analysis stage
## Examples
{:ok, runner} = TinkexRunner.run_stage(runner, :train, %{
type: :train,
config: %{epochs: 5}
})
"""
@spec run_stage(t(), atom(), map()) :: {:ok, t()} | {:error, term()}
def run_stage(%__MODULE__{} = runner, stage_name, stage_config) do
# Check for forced errors (for testing)
if get_in(stage_config, [:config, :force_error]) do
{:error, "Forced error for testing"}
else
# Emit start telemetry
emit_stage_start(runner, stage_name)
runner = %{runner | status: :running}
# Run the appropriate stage
result =
case stage_config.type do
:train ->
run_train_stage(runner, stage_config.config)
:eval ->
run_eval_stage(runner, stage_config.config)
:analysis ->
run_analysis_stage(runner, stage_config.config)
_ ->
{:ok, %{}}
end
case result do
{:ok, stage_result} ->
# Emit stop telemetry
emit_stage_stop(runner, stage_name, stage_result)
# Update results
updated_results = Map.put(runner.results, stage_name, stage_result)
# Update checkpoints if any
new_checkpoints =
case stage_result do
%{checkpoints: cps} -> runner.checkpoints ++ cps
_ -> runner.checkpoints
end
{:ok,
%{
runner
| results: updated_results,
checkpoints: new_checkpoints
}}
{:error, reason} = error ->
emit_stage_error(runner, stage_name, reason)
error
end
end
end
@doc """
Gets the current results from the runner.
## Examples
results = TinkexRunner.get_results(runner)
# => %{train: %{...}, eval: %{...}}
"""
@spec get_results(t()) :: map()
def get_results(%__MODULE__{results: results}) do
results
end
@doc """
Cleans up runner resources.
## Examples
:ok = TinkexRunner.cleanup(runner)
"""
@spec cleanup(t()) :: :ok
def cleanup(%__MODULE__{} = _runner) do
# Clean up any resources
# In a real implementation, this would clean up Tinkex sessions, etc.
:ok
end
# Private functions
defp run_train_stage(runner, config) do
Logger.info("Running training stage for experiment: #{runner.experiment.name}")
# Get training parameters
epochs = Map.get(config, :epochs, 3)
batch_size = Map.get(config, :batch_size, 8)
checkpoint_every = Map.get(config, :checkpoint_every, 100)
# In a real implementation, this would:
# 1. Create a Tinkex training session if not exists
# 2. Load the dataset
# 3. Run the training loop
# 4. Save checkpoints
# Mock training result for now
# Assuming 10 batches per epoch
total_steps = epochs * 10
checkpoints =
for step <- 1..total_steps, rem(step, checkpoint_every) == 0 do
"step-#{step}"
end
result = %{
total_steps: total_steps,
epochs_completed: epochs,
batch_size: batch_size,
metrics: %{
avg_loss: :rand.uniform() * 0.5 + 0.2,
final_loss: :rand.uniform() * 0.3 + 0.1
},
checkpoints: checkpoints
}
{:ok, result}
end
defp run_eval_stage(_runner, config) do
Logger.info("Running evaluation stage")
# Check for forced errors (for testing)
if Map.get(config, :force_error) do
{:error, "Forced evaluation error for testing"}
else
# Get evaluation parameters
_test_data = Map.get(config, :test_data, "default_test")
metrics = Map.get(config, :metrics, [:accuracy])
# In a real implementation, this would:
# 1. Load the test dataset
# 2. Run inference on test samples
# 3. Compute metrics
# Mock evaluation result
result =
Enum.reduce(metrics, %{}, fn metric, acc ->
Map.put(acc, metric, :rand.uniform())
end)
|> Map.merge(%{
schema_compliance: 0.98,
citation_accuracy: 0.96,
mean_entailment: 0.55,
overall_pass_rate: 0.48
})
{:ok, result}
end
end
defp run_analysis_stage(_runner, config) do
Logger.info("Running analysis stage")
# Get analysis parameters
tests = Map.get(config, :tests, [:t_test])
# In a real implementation, this would:
# 1. Load results from previous stages
# 2. Run statistical tests using Crucible.Bench
# 3. Generate analysis results
# Mock analysis result
result = %{
tests_run: tests,
p_values: %{},
effect_sizes: %{},
significant: false
}
{:ok, result}
end
defp emit_stage_start(runner, stage_name) do
:telemetry.execute(
[:crucible, :harness, :stage_start],
%{system_time: System.system_time()},
%{
stage: stage_name,
experiment_id: runner.experiment.id,
experiment_name: runner.experiment.name
}
)
end
defp emit_stage_stop(runner, stage_name, result) do
:telemetry.execute(
[:crucible, :harness, :stage_stop],
%{
system_time: System.system_time(),
result_count: map_size(result)
},
%{
stage: stage_name,
experiment_id: runner.experiment.id,
experiment_name: runner.experiment.name
}
)
end
defp emit_stage_error(runner, stage_name, reason) do
:telemetry.execute(
[:crucible, :harness, :stage_error],
%{system_time: System.system_time()},
%{
stage: stage_name,
experiment_id: runner.experiment.id,
error: reason
}
)
end
end