Skip to content
Merged
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
9 changes: 9 additions & 0 deletions aether/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
10 changes: 10 additions & 0 deletions aether/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion aether/_utils/progress.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion aether/custom_kernels/batchnorm_kernel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 3 additions & 3 deletions aether/layers/activations.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions aether/layers/normalization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 4 additions & 4 deletions aether/losses/categorical_crossentropy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,)
Expand Down
13 changes: 5 additions & 8 deletions aether/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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:
Expand Down
15 changes: 8 additions & 7 deletions aether/preprocessing/transforms.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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).

Expand Down
1 change: 0 additions & 1 deletion examples/cifar10/v2_cifar10.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pathlib import Path

import cupy as cp
import numpy as np

import aether as ae

Expand Down
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 1 addition & 3 deletions tests/base_case.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import unittest
import warnings

import numpy as np

import aether.config as config
from aether.config import set_backend

Expand Down Expand Up @@ -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.
Expand Down
13 changes: 5 additions & 8 deletions tests/config/test_dtype_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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."""
Expand Down
7 changes: 3 additions & 4 deletions tests/integration/model_base_suite.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import math

import aether as ae
import aether.config as config
import tests.base_case as base_case


Expand Down Expand Up @@ -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)):
Expand All @@ -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)

Expand Down
1 change: 0 additions & 1 deletion tests/layers/test_batch_normalization.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/layers/test_flatten.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import aether.config as config
import tests.base_case as base_case
from aether.layers.linear import Flatten

Expand Down
1 change: 0 additions & 1 deletion tests/layers/test_leaky_relu.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import aether.config as config
import tests.base_case as base_case
from aether.layers.activations import LeakyReLU

Expand Down
6 changes: 3 additions & 3 deletions tests/layers/test_max_pool2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down
1 change: 0 additions & 1 deletion tests/layers/test_softmax.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import aether.config as config
import tests.base_case as base_case
from aether.layers.activations import SoftMax

Expand Down
1 change: 0 additions & 1 deletion tests/layers/test_spatial_dropout.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import aether.config as config
import tests.base_case as base_case
from aether.layers.dropout import SpatialDropout

Expand Down
1 change: 0 additions & 1 deletion tests/losses/test_categorical_crossentropy.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/losses/test_softmax_categorical_crossentropy.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/model/test_evaluate.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import contextlib
import io
import unittest

import numpy as np

Expand Down
1 change: 0 additions & 1 deletion tests/model/test_preprocessor_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 1 addition & 5 deletions tests/optimizers/adam_base_suite.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)))
Expand Down
1 change: 0 additions & 1 deletion tests/optimizers/test_optimizer_adam.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion tests/optimizers/test_optimizer_adam_w.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 0 additions & 2 deletions tests/preprocessing/test_deserialize.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
1 change: 0 additions & 1 deletion tests/preprocessing/test_rescale.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
Loading