diff --git a/docs/src/content/docs/docs/guides/pipeline.md b/docs/src/content/docs/docs/guides/pipeline.md index bed7bca..185af8a 100644 --- a/docs/src/content/docs/docs/guides/pipeline.md +++ b/docs/src/content/docs/docs/guides/pipeline.md @@ -114,6 +114,8 @@ results = pipeline.run() results.save(output_dir="my_experiment", model_id=model.model_id) ``` +Both default `output_dir` to `murano_outputs/` in the current directory, and the `Plot` step writes there too, so a run's artifacts and plots share one tree. Results go directly into `output_dir`, so re-running overwrites the previous run; pass `run_name="..."` to keep runs side by side under `murano_outputs//`. + The output structure: ``` diff --git a/src/murano/io.py b/src/murano/io.py index 2b81f0d..7aa6c5c 100644 --- a/src/murano/io.py +++ b/src/murano/io.py @@ -1083,7 +1083,7 @@ def _find_serializer( def save_results( results: Any, - output_dir: str = "murano_outputs", + output_dir: str = keys.DEFAULT_OUTPUT_DIR, model_id: str = "", run_name: str | None = None, ) -> Path: diff --git a/src/murano/keys.py b/src/murano/keys.py index 655d16b..0ba005d 100644 --- a/src/murano/keys.py +++ b/src/murano/keys.py @@ -27,6 +27,11 @@ WEIGHT_ABLATION: Final = "weight_ablation" OUTPUT_DIR: Final = "output_dir" +# Default base directory (relative to the current working directory) for saved +# artifacts, shared by the Save step, Results.save, and the Plot step so they +# all write into one predictable tree. +DEFAULT_OUTPUT_DIR: Final = "murano_outputs" + # Default keys for the configurable metric steps (callers may override these). FINAL_LOGITS: Final = "final_logits" TARGET_IDS: Final = "target_ids" diff --git a/src/murano/results.py b/src/murano/results.py index c100053..6c4d92b 100644 --- a/src/murano/results.py +++ b/src/murano/results.py @@ -4,6 +4,8 @@ from typing import Any +from murano.keys import DEFAULT_OUTPUT_DIR + class Results: """Dict-like container for pipeline step outputs. @@ -60,7 +62,7 @@ def copy(self) -> Results: def save( self, - output_dir: str = "murano_outputs", + output_dir: str = DEFAULT_OUTPUT_DIR, run_name: str | None = None, model_id: str = "", ): diff --git a/src/murano/steps/plot.py b/src/murano/steps/plot.py index f9b1623..9b2a611 100644 --- a/src/murano/steps/plot.py +++ b/src/murano/steps/plot.py @@ -53,7 +53,8 @@ class Plot(Step): ``plot_sae_token_activations``, ...) directly instead. Args: - output_dir: Root output directory. If None, uses results['output_dir']. + output_dir: Root output directory. If None, uses results['output_dir'] + when a preceding Save set it, otherwise ``murano_outputs``. show: Display each figure inline when run in a notebook. Defaults to True. save_format: File format for saved figures. ``"png"`` (default) writes a static image via kaleido and falls back to a self-contained @@ -94,7 +95,7 @@ def __call__(self, results: Results) -> Results: root = ( Path(self.output_dir) if self.output_dir - else Path(results.get(keys.OUTPUT_DIR, ".")) + else Path(results.get(keys.OUTPUT_DIR, keys.DEFAULT_OUTPUT_DIR)) ) plots_dir = root / "plots" plots_dir.mkdir(parents=True, exist_ok=True) diff --git a/src/murano/steps/save.py b/src/murano/steps/save.py index e9d65bb..976ec57 100644 --- a/src/murano/steps/save.py +++ b/src/murano/steps/save.py @@ -26,23 +26,34 @@ class Save(Step): results['output_dir']: Path to the output directory. Args: - output_dir: Base directory for outputs. + output_dir: Base directory for outputs. Defaults to ``murano_outputs`` + in the current working directory. model_id: Model identifier for metadata. + run_name: Optional subdirectory inside ``output_dir`` for this run. By + default results are written directly into ``output_dir`` and a + re-run overwrites them; set ``run_name`` to keep runs separate. """ reads = [] writes = [keys.OUTPUT_DIR] write_types = {keys.OUTPUT_DIR: Path} - def __init__(self, output_dir: str = "murano_outputs", model_id: str = ""): + def __init__( + self, + output_dir: str = keys.DEFAULT_OUTPUT_DIR, + model_id: str = "", + run_name: str | None = None, + ): self.output_dir = output_dir self.model_id = model_id + self.run_name = run_name def __call__(self, results: Results) -> Results: out_dir = save_results( results, output_dir=self.output_dir, model_id=self.model_id, + run_name=self.run_name, ) results[keys.OUTPUT_DIR] = out_dir return results diff --git a/tests/test_coverage.py b/tests/test_coverage.py index b9db632..a1f293d 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -191,3 +191,28 @@ def test_plot_step_html_format_writes_html(tmp_path): ) Plot(output_dir=str(tmp_path), save_format="html")(results) assert (tmp_path / "plots" / "separation_scores.html").exists() + + +def test_plot_step_defaults_to_shared_output_root(tmp_path, monkeypatch): + pytest.importorskip("plotly") + from murano import keys + from murano.results import Results + from murano.steps.plot import Plot + from murano.steps.train import SteeringResult + + monkeypatch.chdir(tmp_path) + results = Results() + results[keys.STEERING] = SteeringResult( + direction_per_layer={ + (0, "residual"): torch.ones(4), + (1, "residual"): torch.ones(4), + }, + separation_scores={(0, "residual"): 1.0, (1, "residual"): 0.5}, + best_layer=(0, "residual"), + ) + # No output_dir and no results['output_dir']: writes under the shared + # murano_outputs/ root, not a bare ./plots/ in the CWD. + Plot(save_format="html")(results) + plots = tmp_path / keys.DEFAULT_OUTPUT_DIR / "plots" + assert (plots / "separation_scores.html").exists() + assert not (tmp_path / "plots").exists() diff --git a/tests/test_steps.py b/tests/test_steps.py index d46bf24..1739fcf 100644 --- a/tests/test_steps.py +++ b/tests/test_steps.py @@ -581,6 +581,14 @@ def test_save_serializes_prompt_and_metric_artifacts(self, tmp_path): assert (out_dir / "evaluation" / "generations.json").exists() assert (out_dir / "metrics" / "metric.json").exists() + def test_save_step_passes_run_name(self, tmp_path): + from murano.steps.save import Save + + r = Results() + Save(output_dir=str(tmp_path), run_name="run1")(r) + assert (tmp_path / "run1" / "metadata.json").exists() + assert r["output_dir"] == tmp_path / "run1" + # ── Probing Fixtures ──────────────────────────────────────────────────