diff --git a/aether/__init__.py b/aether/__init__.py index 21f309a..190946a 100644 --- a/aether/__init__.py +++ b/aether/__init__.py @@ -1,3 +1,12 @@ +"""Aether-ML: a deep learning framework built on NumPy and CuPy, with hand-written +CUDA and ROCm kernels. + +Model, layers, losses, metrics, optimizers and preprocessing transforms are loaded +lazily on first attribute access, so ``import aether`` stays cheap. + +The array backend is selected with ``Model.to()``: ``"numpy"`` runs on the CPU, and +``"cupy"`` runs on the GPU using the hand-written kernels where the device supports them. +""" from importlib.metadata import PackageNotFoundError from importlib.metadata import version as _dist_version diff --git a/aether/__init__.pyi b/aether/__init__.pyi index d04e571..c90169d 100644 --- a/aether/__init__.pyi +++ b/aether/__init__.pyi @@ -5,6 +5,16 @@ # gets real autocomplete/type info despite the runtime # __getattr__ lazy loading. +"""Aether-ML: a deep learning framework built on NumPy and CuPy, with hand-written +CUDA and ROCm kernels. + +Model, layers, losses, metrics, optimizers and preprocessing transforms are loaded +lazily on first attribute access, so ``import aether`` stays cheap. + +The array backend is selected with ``Model.to()``: ``"numpy"`` runs on the CPU, and +``"cupy"`` runs on the GPU using the hand-written kernels where the device supports them. +""" + from aether.layers.activations import LeakyReLU as LeakyReLU from aether.layers.activations import ReLU as ReLU from aether.layers.activations import SoftMax as SoftMax diff --git a/aether/_utils/progress.py b/aether/_utils/progress.py index 0630f17..ad0f931 100644 --- a/aether/_utils/progress.py +++ b/aether/_utils/progress.py @@ -10,7 +10,8 @@ import shutil import sys import time -from typing import Callable, Literal +from collections.abc import Callable +from typing import Literal from aether._utils.null_objects import _NullProgress diff --git a/aether/custom_kernels/batchnorm_kernel.py b/aether/custom_kernels/batchnorm_kernel.py index fa6477e..15fbba8 100644 --- a/aether/custom_kernels/batchnorm_kernel.py +++ b/aether/custom_kernels/batchnorm_kernel.py @@ -281,7 +281,7 @@ def get_bn_backward_finalize_kernel(variant: str): # --- Availability Helper -------------------------------------------------------- -def is_gpu_bn_available(variant: str = None) -> bool: +def is_gpu_bn_available(variant: str | None = None) -> bool: """Checks if CuPy hardware support and BatchNorm kernels are available.""" if variant is None: variant, _ = config.resolve_gpu_launch_geometry() diff --git a/aether/layers/activations.py b/aether/layers/activations.py index 6757f88..3b920a3 100644 --- a/aether/layers/activations.py +++ b/aether/layers/activations.py @@ -118,9 +118,9 @@ class SoftMax(Layer): Notes ----- - - **Expected input shape:** ``(batch_size, n_classes)`` - - **Output shape:** ``(batch_size, n_classes)`` - - **Precision policy:** Exempt from mixed-precision downcasting; forces + - Expected input shape: ``(batch_size, n_classes)`` + - Output shape: ``(batch_size, n_classes)`` + - Precision policy: Exempt from mixed-precision downcasting; forces intermediate arrays and outputs to ``float32``. """ _precision_exempt = True diff --git a/aether/layers/normalization.py b/aether/layers/normalization.py index ec2aace..fce991f 100644 --- a/aether/layers/normalization.py +++ b/aether/layers/normalization.py @@ -22,13 +22,13 @@ class BatchNorm(Layer): Notes ----- - - **Expected layout:** Channels-last. Operates over 2D inputs of shape + - Expected layout: Channels-last. Operates over 2D inputs of shape ``(batch_size, n_features)`` or 4D spatial inputs of shape ``(batch_size, height, width, n_channels)``. - - **Shape contract:** + - Shape contract: - Input: ``(d_0, d_1, ..., d_{k-1}, C)`` - Output: ``(d_0, d_1, ..., d_{k-1}, C)`` (identical to input) - - **Training vs. Evaluation:** + - Training vs. Evaluation: - In training mode (``training=True``), mean and variance are computed from the current mini-batch across all leading axes, and running statistics are updated. diff --git a/aether/losses/categorical_crossentropy.py b/aether/losses/categorical_crossentropy.py index 8506e3b..f19fe03 100644 --- a/aether/losses/categorical_crossentropy.py +++ b/aether/losses/categorical_crossentropy.py @@ -49,7 +49,7 @@ def validate_graph(self, layers: list): last_layer = layers[-1] if isinstance(last_layer, self.prohibited_preceding_layers): prohibited_names = ", ".join(cls.__name__ for cls in self.prohibited_preceding_layers) - raise ValueError( + raise ValueError( # noqa: TRY004 - graph errors are a ValueError by contract f"[aether] '{type(self).__name__}' operates directly on unnormalized logits. " f"Do not add an explicit '{prohibited_names}' layer immediately preceding this loss." ) @@ -126,7 +126,7 @@ class CategoricalCrossEntropy(Loss): Notes ----- - - **Expected inputs:** + - Expected inputs: - ``y_pred``: Probability distributions of shape ``(batch_size, n_classes)``. Must already be normalized (e.g., via preceding ``SoftMax`` layer). - ``y_true``: Target labels as sparse integer indices ``(batch_size,)`` or @@ -187,11 +187,11 @@ class SoftmaxCategoricalCrossEntropy(Loss): Notes ----- - - **Expected inputs:** + - Expected inputs: - ``logits``: Raw, unnormalized activations of shape ``(batch_size, n_classes)``. - ``y_true``: Target labels as sparse integer indices ``(batch_size,)`` or one-hot distributions ``(batch_size, n_classes)``. - - **Graph constraint:** Operates directly on logits; preceding this loss with an + - Graph constraint: Operates directly on logits; preceding this loss with an explicit ``SoftMax`` layer is prohibited to prevent redundant double normalization. """ prohibited_preceding_layers = (SoftMax,) diff --git a/aether/model.py b/aether/model.py index 061ae8c..55c364a 100644 --- a/aether/model.py +++ b/aether/model.py @@ -745,10 +745,9 @@ def train( Defaults to True. print_every (int): Step interval frequency for logging telemetry. Ignored when `verbose=0`. - verbose (int, optional): Verbosity mode for training telemetry. - - `0`: Silent mode (no output printed). - - `1`: Dynamic graphical progress bar with metrics (default). - - `2`: Plain text summary per epoch without carriage returns/ANSI escapes. + verbose (int, optional): Telemetry verbosity: 0 is silent, 1 shows a dynamic + progress bar with metrics (default), 2 prints a plain text summary per epoch + without carriage returns or ANSI escapes. validation_data (tuple, optional): Raw (X_val, y_val) tuple evaluated per epoch. fit_preprocessor (bool, optional): Whether to fit the preprocessor (None=only if unfitted, True=always, False=never). @@ -911,10 +910,8 @@ def evaluate(self, X, y, *, batch_size=None, verbose=1): Raw/un-preprocessed when a preprocessing pipeline is attached. y (ndarray): Ground truth labels or targets. batch_size (int, optional): Mini-batch size. Defaults to None (full-batch). - verbose (int | bool, optional): Verbosity mode for evaluation. - - `0` / `False`: Silent mode (no output printed). - - `1` / `True`: Prints the final evaluation loss and accuracy metrics. - Defaults to 1. + verbose (int | bool, optional): Verbosity for evaluation: 0 / False is silent, + 1 / True prints the final evaluation loss and accuracy metrics. Defaults to 1. Returns: tuple: (val_loss, val_acc) representing the computed validation metrics. Raises: diff --git a/aether/preprocessing/transforms.py b/aether/preprocessing/transforms.py index 997ca58..9b4e4be 100644 --- a/aether/preprocessing/transforms.py +++ b/aether/preprocessing/transforms.py @@ -1,5 +1,6 @@ import warnings -from typing import Any, Optional, Sequence, Tuple, Union +from collections.abc import Sequence +from typing import Any import numpy as np @@ -20,10 +21,10 @@ def _dtype_name(dt): def to_tensor( *args: Any, - target_device: Optional[str] = None, - dtype: Union[str, type, np.dtype, Sequence[Union[str, type, np.dtype]], None] = None, + target_device: str | None = None, + dtype: str | type | np.dtype | Sequence[str | type | np.dtype] | None = None, preserve_integers: bool = True -) -> Union[Any, Tuple[Any, ...], None]: +) -> Any | tuple[Any, ...] | None: """Converts array-like inputs into backend tensors (NumPy or CuPy). Flexible API for precision handling and hardware device placement. Accepts @@ -566,7 +567,7 @@ def from_config(cls, cfg): return cls(transforms=[deserialize(entry) for entry in cfg.get("transforms") or []]) -def deserialize(entry: dict | None) -> Optional[Preprocess]: +def deserialize(entry: dict | None) -> Preprocess | None: """Reconstructs a Preprocess transform or pipeline from a serialized configuration dictionary. This function acts as the universal factory and resolver for preprocessing @@ -576,9 +577,9 @@ def deserialize(entry: dict | None) -> Optional[Preprocess]: When is this used? ------------------ - - **Internally:** Called automatically by `Model.load()` to restore attached + - Internally: Called automatically by `Model.load()` to restore attached data preprocessing pipelines from saved `.aether` model archives. - - **Standalone:** Called by users to load or transfer preprocessor pipelines + - Standalone: Called by users to load or transfer preprocessor pipelines independently of a model (e.g., loading fitted scalers into external APIs, ETL data workers, or configuration-driven workflows). diff --git a/examples/cifar10/v2_cifar10.py b/examples/cifar10/v2_cifar10.py index 42ce51d..31722a3 100644 --- a/examples/cifar10/v2_cifar10.py +++ b/examples/cifar10/v2_cifar10.py @@ -4,7 +4,6 @@ from pathlib import Path import cupy as cp -import numpy as np import aether as ae diff --git a/pyproject.toml b/pyproject.toml index 8b48252..744426b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,6 +60,12 @@ extend-exclude = ["notebooks", "examples/legacy", "*.ipynb"] # Allow `import aether.config as config`; preferred here for readability. ignore = ["PLR0402"] +[tool.ruff.lint.per-file-ignores] +# Skeleton templates keep placeholder imports and `pass` bodies on purpose. +"tests/integration/_integration_test_skeleton.py" = ["F401", "PIE790"] +# Stub `__all__` lists are grouped by topic, not sorted. +"*.pyi" = ["RUF022"] + [tool.ruff.lint.flake8-comprehensions] # Allow dict(key=value) for kernel template/kwargs bundles. allow-dict-calls-with-keyword-arguments = true diff --git a/tests/base_case.py b/tests/base_case.py index 844743e..da8651b 100644 --- a/tests/base_case.py +++ b/tests/base_case.py @@ -1,8 +1,6 @@ import unittest import warnings -import numpy as np - import aether.config as config from aether.config import set_backend @@ -55,7 +53,7 @@ def setUp(self): def shortDescription(self): """Docstrings are ommitted when running verbose -v unit tests""" - return None + return def make_built_layer(self, layer_cls, input_shape: tuple[int, ...], seed: int | None = None, **kwargs): """Constructs, binds to the active device backend, and builds a layer instance. diff --git a/tests/config/test_dtype_policy.py b/tests/config/test_dtype_policy.py index b8b75d2..bd7ec16 100644 --- a/tests/config/test_dtype_policy.py +++ b/tests/config/test_dtype_policy.py @@ -2,9 +2,8 @@ import numpy as np -import aether.config as config import tests.base_case as base_case -from aether.config import COMPUTE_DTYPE, PARAM_DTYPE, DTypePolicy +from aether.config import PARAM_DTYPE, DTypePolicy class TestDTypePolicy(base_case.AetherBaseTestCase): @@ -34,17 +33,15 @@ def test_invalid_string_raises_value_error(self): """Ensure passing strings outside COMPUTE_DTYPE raises ValueError.""" invalid_dtypes = ['int8', 'int32', 'float8', 'complex64', 'invalid_type', 'bfloat16'] for dt in invalid_dtypes: - with self.subTest(dtype=dt): - with self.assertRaises(ValueError): - DTypePolicy(compute_dtype=dt) + with self.subTest(dtype=dt), self.assertRaises(ValueError): + DTypePolicy(compute_dtype=dt) def test_non_string_non_none_raises_type_error(self): """Ensure non-str and non-None inputs raise TypeError.""" invalid_types = [123, 32.0, ['float32'], {'dtype': 'float32'}, True] for val in invalid_types: - with self.subTest(val=val): - with self.assertRaises(TypeError): - DTypePolicy(compute_dtype=val) + with self.subTest(val=val), self.assertRaises(TypeError): + DTypePolicy(compute_dtype=val) def test_valid_initializations(self): """Verify valid compute_dtypes set attributes correctly.""" diff --git a/tests/integration/model_base_suite.py b/tests/integration/model_base_suite.py index eb37907..7b2acd2 100644 --- a/tests/integration/model_base_suite.py +++ b/tests/integration/model_base_suite.py @@ -4,7 +4,6 @@ import math import aether as ae -import aether.config as config import tests.base_case as base_case @@ -37,9 +36,9 @@ def build_cnn_model(self, *, device=None, precision=None, input_dim=(32, 32, 3)) optimizer=ae.Adam(lr=0.001, decay=5e-5), accuracy=ae.CategoricalAccuracy(), ) - model.to(device if device is not None else self.backend_name) + model.to(target_device) model.manual_seed(seed=42) - + return self._place_and_finalize(model, precision=precision, input_dim=input_dim) def build_mlp_model(self, *, device=None, precision=None, input_dim=(32, 32, 3)): @@ -54,7 +53,7 @@ def build_mlp_model(self, *, device=None, precision=None, input_dim=(32, 32, 3)) optimizer=ae.Adam(lr=0.001, decay=5e-5), accuracy=ae.CategoricalAccuracy(), ) - model.to(device if device is not None else self.backend_name) + model.to(target_device) model.manual_seed(seed=42) return self._place_and_finalize(model, precision=precision, input_dim=input_dim) diff --git a/tests/layers/test_batch_normalization.py b/tests/layers/test_batch_normalization.py index b0a226c..6886de4 100644 --- a/tests/layers/test_batch_normalization.py +++ b/tests/layers/test_batch_normalization.py @@ -1,4 +1,3 @@ -import aether.config as config import aether.custom_kernels.relu_kernel as gpu_relu import tests.base_case as base_case from aether.layers.activations import LeakyReLU, ReLU diff --git a/tests/layers/test_flatten.py b/tests/layers/test_flatten.py index d0a26f7..20c8c01 100644 --- a/tests/layers/test_flatten.py +++ b/tests/layers/test_flatten.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.linear import Flatten diff --git a/tests/layers/test_leaky_relu.py b/tests/layers/test_leaky_relu.py index 9bb5f88..900d9a6 100644 --- a/tests/layers/test_leaky_relu.py +++ b/tests/layers/test_leaky_relu.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.activations import LeakyReLU diff --git a/tests/layers/test_max_pool2d.py b/tests/layers/test_max_pool2d.py index 13aecb0..105d7b0 100644 --- a/tests/layers/test_max_pool2d.py +++ b/tests/layers/test_max_pool2d.py @@ -244,9 +244,9 @@ def test_same_padding_boundary_scaling(self): def test_build_invalid_padding_raises_value_error(self): with self.assertRaises(ValueError): - layer = self.make_built_layer( - MaxPool2d, - input_shape=self.INPUT_SHAPE[1:], + self.make_built_layer( + MaxPool2d, + input_shape=self.INPUT_SHAPE[1:], filter_size=(2, 2), stride=(2, 2), padding='not_a_real_padding') diff --git a/tests/layers/test_softmax.py b/tests/layers/test_softmax.py index 895356b..b53731c 100644 --- a/tests/layers/test_softmax.py +++ b/tests/layers/test_softmax.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.activations import SoftMax diff --git a/tests/layers/test_spatial_dropout.py b/tests/layers/test_spatial_dropout.py index 0ef8c14..24bd610 100644 --- a/tests/layers/test_spatial_dropout.py +++ b/tests/layers/test_spatial_dropout.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.dropout import SpatialDropout diff --git a/tests/losses/test_categorical_crossentropy.py b/tests/losses/test_categorical_crossentropy.py index 66dc794..efe64e7 100644 --- a/tests/losses/test_categorical_crossentropy.py +++ b/tests/losses/test_categorical_crossentropy.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.linear import Dense from aether.losses.categorical_crossentropy import CategoricalCrossEntropy diff --git a/tests/losses/test_softmax_categorical_crossentropy.py b/tests/losses/test_softmax_categorical_crossentropy.py index 47238c4..2a64167 100644 --- a/tests/losses/test_softmax_categorical_crossentropy.py +++ b/tests/losses/test_softmax_categorical_crossentropy.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.activations import SoftMax from aether.layers.linear import Dense diff --git a/tests/model/test_evaluate.py b/tests/model/test_evaluate.py index cca4f73..087fcf9 100644 --- a/tests/model/test_evaluate.py +++ b/tests/model/test_evaluate.py @@ -1,6 +1,5 @@ import contextlib import io -import unittest import numpy as np diff --git a/tests/model/test_preprocessor_serialization.py b/tests/model/test_preprocessor_serialization.py index ee7d6f8..6add677 100644 --- a/tests/model/test_preprocessor_serialization.py +++ b/tests/model/test_preprocessor_serialization.py @@ -7,7 +7,6 @@ import numpy as np -import aether.config as config import tests.base_case as base_case from aether._utils import NullPreprocessor from aether.layers import Dense, ReLU diff --git a/tests/optimizers/adam_base_suite.py b/tests/optimizers/adam_base_suite.py index 0e6e98f..f376fe4 100644 --- a/tests/optimizers/adam_base_suite.py +++ b/tests/optimizers/adam_base_suite.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.layers.conv import Conv2d from aether.layers.linear import Dense @@ -275,10 +274,7 @@ def test_gpu_kernel_execution(self): optimizer.init_params([layer]) optimizer._compile_for_device("cupy") - try: - optimizer.step() - except Exception as e: - self.fail(f"CuPy kernel failed during optimizer.step(): {e}") + optimizer.step() self.assertTrue(self.xp.all(self.xp.isfinite(layer.weights))) self.assertTrue(self.xp.all(self.xp.isfinite(layer.biases))) diff --git a/tests/optimizers/test_optimizer_adam.py b/tests/optimizers/test_optimizer_adam.py index 2737eb5..9208d15 100644 --- a/tests/optimizers/test_optimizer_adam.py +++ b/tests/optimizers/test_optimizer_adam.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.optimizers.adam import Adam from tests.optimizers.adam_base_suite import BaseTestOptimizerAdam diff --git a/tests/optimizers/test_optimizer_adam_w.py b/tests/optimizers/test_optimizer_adam_w.py index 68d0f8d..0216b0c 100644 --- a/tests/optimizers/test_optimizer_adam_w.py +++ b/tests/optimizers/test_optimizer_adam_w.py @@ -1,4 +1,3 @@ -import aether.config as config import tests.base_case as base_case from aether.optimizers.adam import AdamW from tests.optimizers.adam_base_suite import BaseTestOptimizerAdam diff --git a/tests/preprocessing/test_deserialize.py b/tests/preprocessing/test_deserialize.py index 6f04ce2..fc17eae 100644 --- a/tests/preprocessing/test_deserialize.py +++ b/tests/preprocessing/test_deserialize.py @@ -1,9 +1,7 @@ -import numpy as np import tests.base_case as base_case from aether.preprocessing.transforms import ( Compose, - Preprocess, Rescale, StandardScaler, ToTensor, diff --git a/tests/preprocessing/test_rescale.py b/tests/preprocessing/test_rescale.py index 4984d9c..48192b6 100644 --- a/tests/preprocessing/test_rescale.py +++ b/tests/preprocessing/test_rescale.py @@ -1,6 +1,5 @@ import numpy as np -import aether.config as config import tests.base_case as base_case from aether.preprocessing.transforms import Rescale