Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/oumi/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
from oumi.cli.synth import synth
from oumi.cli.train import train
from oumi.cli.tune import tune
from oumi.exceptions import OumiConfigError
from oumi.utils.logging import should_use_rich_logging

_ASCII_LOGO = r"""
Expand Down Expand Up @@ -365,6 +366,9 @@ def run():
telemetry = TelemetryManager.get_instance()
with telemetry.capture_operation(event_name, event_properties):
return app()
except OumiConfigError as e:
CONSOLE.print(f"[red]Error: {e}[/red]")
sys.exit(1)
except Exception as e:
tb_str = traceback.format_exc()
CONSOLE.print(tb_str)
Expand Down
3 changes: 3 additions & 0 deletions src/oumi/core/configs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,12 +158,14 @@
from oumi.core.configs.synthesis_config import SynthesisConfig
from oumi.core.configs.training_config import TrainingConfig
from oumi.core.configs.tuning_config import TuningConfig
from oumi.exceptions import OumiConfigError, OumiConfigFileNotFoundError

__all__ = [
"AsyncEvaluationConfig",
"AutoWrapPolicy",
"BackwardPrefetch",
"BaseConfig",
"OumiConfigFileNotFoundError",
"DataParams",
"DatasetParams",
"DatasetSplit",
Expand Down Expand Up @@ -192,6 +194,7 @@
"MixedPrecisionDtype",
"MixtureStrategy",
"ModelParams",
"OumiConfigError",
"PeftParams",
"PeftSaveMode",
"ProfilerParams",
Expand Down
15 changes: 14 additions & 1 deletion src/oumi/core/configs/base_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from omegaconf import OmegaConf

from oumi.core.configs.params.base_params import BaseParams
from oumi.exceptions import OumiConfigError, OumiConfigFileNotFoundError

T = TypeVar("T", bound="BaseConfig")

Expand Down Expand Up @@ -128,6 +129,10 @@ def _read_config_without_interpolation(config_path: str) -> str:
Returns:
str: The stringified configuration.
"""
if not Path(config_path).is_file():
raise OumiConfigFileNotFoundError(
f"Config file not found or path is not a file: {config_path}"
)
with open(config_path) as f:
stringified_config = f.read()
pattern = r"(?<!\\)\$\{" # Matches "${" but not "\${"
Expand Down Expand Up @@ -165,7 +170,11 @@ def to_yaml(self, config_path: str | Path | StringIO) -> None:
+ "\n".join(f"- {path}" for path in sorted(removed_paths))
)

OmegaConf.save(config=processed_config, f=config_path)
try:
OmegaConf.save(config=processed_config, f=config_path)
except OSError as e:
# handle missing parent folder
raise OumiConfigError(f"Failed to save config to {config_path}: {e}") from e

@classmethod
def from_yaml(
Expand All @@ -181,6 +190,10 @@ def from_yaml(
Returns:
BaseConfig: The merged configuration object.
"""
if not Path(config_path).is_file():
raise OumiConfigFileNotFoundError(
f"Config file not found or path is not a file: {config_path}"
)
schema = OmegaConf.structured(cls)
if ignore_interpolation:
stringified_config = _read_config_without_interpolation(str(config_path))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

"""Backward-compatible re-exports. Canonical definitions live in oumi.exceptions."""

class HardwareException(Exception):
"""An exception thrown for invalid hardware configurations."""
from oumi.exceptions import OumiConfigError, OumiConfigFileNotFoundError

__all__ = ["OumiConfigFileNotFoundError", "OumiConfigError"]
9 changes: 9 additions & 0 deletions src/oumi/core/configs/params/data_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@
import warnings
from dataclasses import dataclass, field, fields
from enum import Enum
from pathlib import Path
from typing import Any, Literal

from omegaconf import MISSING

from oumi.core.configs.params.base_params import BaseParams
from oumi.exceptions import ConfigFileNotFoundError


# Training Params
Expand Down Expand Up @@ -182,6 +184,13 @@ def __post_init__(self):
"Use properties of DatasetParams instead."
)

def __finalize_and_validate__(self):
"""Verifies params."""
if self.dataset_path and not Path(self.dataset_path).exists():
Comment thread
oelachqar marked this conversation as resolved.
raise ConfigFileNotFoundError(
f"dataset_path '{self.dataset_path}' does not exist."
)


@dataclass
class DatasetSplitParams(BaseParams):
Expand Down
26 changes: 23 additions & 3 deletions src/oumi/core/configs/params/model_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@
from transformers.utils import find_adapter_config_file, is_flash_attn_2_available

from oumi.core.configs.params.base_params import BaseParams
from oumi.core.types.exceptions import HardwareException
from oumi.exceptions import (
HardwareException,
OumiConfigError,
OumiConfigFileNotFoundError,
)
from oumi.utils.logging import logger
from oumi.utils.torch_utils import get_torch_dtype

Expand Down Expand Up @@ -268,6 +272,11 @@ def __finalize_and_validate__(self):
adapter_config_file = None
# If this check fails, it means this is not a LoRA model.
if adapter_config_file:
if not Path(adapter_config_file).is_file():
raise OumiConfigFileNotFoundError(
f"Adapter config file not found or path is not a file: "
f"{adapter_config_file}"
)
# If `model_name` is a local dir, this should be the same.
# If it's a HF Hub repo, this should be the path to the cached repo.
adapter_dir = Path(adapter_config_file).parent
Expand All @@ -280,8 +289,19 @@ def __finalize_and_validate__(self):
# present, set it to the base model name found in the adapter config,
# if present. Error otherwise.
if len(list(adapter_dir.glob("config.json"))) == 0:
with open(adapter_config_file) as f:
adapter_config = json.load(f)
try:
with open(adapter_config_file) as f:
adapter_config = json.load(f)
except OSError as e:
raise OumiConfigError(
f"Failed to read adapter config at "
f"{adapter_config_file}: {e}"
) from e
except json.JSONDecodeError as e:
raise OumiConfigError(
f"Adapter config at {adapter_config_file} contains invalid "
f"JSON: (line {e.lineno}, col {e.colno}): {e.msg}"
) from e
model_name = adapter_config.get("base_model_name_or_path")
if not model_name:
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion src/oumi/core/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
TemplatedMessage,
Type,
)
from oumi.core.types.exceptions import HardwareException
from oumi.exceptions import HardwareException

__all__ = [
"HardwareException",
Expand Down
31 changes: 31 additions & 0 deletions src/oumi/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2025 - Oumi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Oumi exception hierarchy.

This module is intentionally free of heavy dependencies (torch, transformers, etc.)
so that it can be imported cheaply in lightweight entry-points such as the CLI.
"""


class OumiConfigError(Exception):
"""Base class for all configuration-related errors."""


class OumiConfigFileNotFoundError(OumiConfigError, FileNotFoundError):
"""A configuration file path does not exist."""


class HardwareException(Exception):
"""An exception thrown for invalid hardware configurations."""
4 changes: 3 additions & 1 deletion tests/unit/cli/test_cli_launch.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,7 +710,9 @@ def test_launch_up_job_not_found(
)
if res.exception:
raise res.exception
assert "No such file or directory" in str(exception_info.value)
assert "Config file not found or path is not a file" in str(
exception_info.value
)


def test_launch_run_job(
Expand Down
37 changes: 37 additions & 0 deletions tests/unit/core/configs/params/test_data_params.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import dataclasses
from pathlib import Path

import pytest

from oumi.core.configs.params.data_params import DatasetParams
from oumi.exceptions import ConfigFileNotFoundError


def _get_invalid_field_name_lists() -> list[list[str]]:
Expand Down Expand Up @@ -37,3 +39,38 @@ def test_dataset_params_reserved_kwargs(field_names: list[str]):
dataset_name="DUMMY-NON-EXISTENT",
dataset_kwargs={field_name: "foo_value" for field_name in field_names},
)


def test_dataset_params_finalize_nonexistent_path():
params = DatasetParams(
dataset_name="some_dataset",
dataset_path="/nonexistent/path/to/data.jsonl",
)
with pytest.raises(
ConfigFileNotFoundError,
match="dataset_path '/nonexistent/path/to/data.jsonl' does not exist",
):
params.finalize_and_validate()


def test_dataset_params_finalize_existing_path(tmp_path: Path):
data_file = tmp_path / "train.jsonl"
data_file.write_text("{}\n")
params = DatasetParams(
dataset_name="some_dataset",
dataset_path=str(data_file),
)
params.finalize_and_validate()


def test_dataset_params_finalize_no_path():
params = DatasetParams(dataset_name="some_dataset")
params.finalize_and_validate()


def test_dataset_params_finalize_existing_directory(tmp_path: Path):
params = DatasetParams(
dataset_name="some_dataset",
dataset_path=str(tmp_path),
)
params.finalize_and_validate()
42 changes: 42 additions & 0 deletions tests/unit/core/configs/params/test_model_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from oumi.core.configs.params.model_params import ModelParams
from oumi.exceptions import OumiConfigError, OumiConfigFileNotFoundError


def test_post_init_adapter_model_present():
Expand Down Expand Up @@ -130,3 +131,44 @@ def test_chat_template_kwargs_custom_assignment():
model_params = ModelParams(chat_template_kwargs={"enable_thinking": False})
assert model_params.chat_template_kwargs is not None
assert model_params.chat_template_kwargs["enable_thinking"] is False


@patch("oumi.core.configs.params.model_params.find_adapter_config_file")
def test_adapter_config_file_path_not_a_file(mock_find, tmp_path: Path):
"""Raises OumiConfigFileNotFoundError for a missing adapter config path."""
mock_find.return_value = str(tmp_path / "ghost_adapter_config.json")

params = ModelParams(model_name=str(tmp_path))
with pytest.raises(
OumiConfigFileNotFoundError,
match="Adapter config file not found or path is not a file",
):
params.finalize_and_validate()


@patch("oumi.core.configs.params.model_params.find_adapter_config_file")
def test_adapter_config_read_oserror(mock_find, tmp_path: Path):
"""Test OSError reading adapter_config.json is re-raised as OumiConfigError."""
adapter_path = tmp_path / "adapter_config.json"
adapter_path.write_text('{"base_model_name_or_path": "base_model"}')
mock_find.return_value = str(adapter_path)

params = ModelParams(model_name=str(tmp_path))
with patch("builtins.open", side_effect=OSError("Permission denied")):
with pytest.raises(
OumiConfigError,
match="Failed to read adapter config",
):
params.finalize_and_validate()


def test_adapter_config_invalid_json(tmp_path: Path):
"""Test malformed JSON raises OumiConfigError with location info."""
(tmp_path / "adapter_config.json").write_text("{not: valid json!!}")

params = ModelParams(model_name=str(tmp_path))
with pytest.raises(OumiConfigError, match="contains invalid JSON") as exc_info:
params.finalize_and_validate()

assert "line" in str(exc_info.value)
assert "col" in str(exc_info.value)
Loading
Loading