Skip to content

memory

Memory-tier helpers for Apple Silicon MLX/MPS experiments.

macOS does not expose a perfect "pretend this machine only has 16 GB unified memory" switch. MLX can cap the allocator used by the Apple-native DiT path, and PyTorch MPS exposes process-level watermark environment variables for the hybrid prompt/decode stages. Applying both gives benchmark and generation entrypoints a practical, explicit way to exercise memory-tier presets.

Classes

fastvideo.mlx_runtime.memory.AppliedMemoryLimits dataclass

AppliedMemoryLimits(mlx_memory_limit_gib: float | None = None, mlx_cache_limit_gib: float | None = None, mlx_disable_cache: bool = False, mlx_wired_limit_gib: float | None = None, torch_mps_high_watermark_ratio: float | None = None, torch_mps_low_watermark_ratio: float | None = None, applied_bytes: dict[str, int] = dict(), previous_bytes: dict[str, int] = dict(), errors: dict[str, str] = dict())

Memory limits applied for one Apple Silicon benchmark/generation process.

Methods:

fastvideo.mlx_runtime.memory.AppliedMemoryLimits.as_metrics
as_metrics() -> dict[str, int | float | str | bool | None]

Flatten the configured memory limits, applied values, previous values, and errors into a metrics dictionary.

Returns:

Type Description
dict[str, int | float | str | bool | None]

dict[str, int | float | str | bool | None]: Metrics keyed by limit names and their corresponding values.

Source code in fastvideo/mlx_runtime/memory.py
def as_metrics(self) -> dict[str, int | float | str | bool | None]:
    """Flatten the configured memory limits, applied values, previous values, and errors into a metrics dictionary.

    Returns:
        dict[str, int | float | str | bool | None]: Metrics keyed by limit names and their corresponding values.
    """
    metrics: dict[str, int | float | str | bool | None] = {
        "mlx_memory_limit_gib": self.mlx_memory_limit_gib,
        "mlx_cache_limit_gib": self.mlx_cache_limit_gib,
        "mlx_disable_cache": self.mlx_disable_cache,
        "mlx_wired_limit_gib": self.mlx_wired_limit_gib,
        "torch_mps_high_watermark_ratio": self.torch_mps_high_watermark_ratio,
        "torch_mps_low_watermark_ratio": self.torch_mps_low_watermark_ratio,
    }
    for name, value in self.applied_bytes.items():
        metrics[f"{name}_bytes"] = value
    for name, value in self.previous_bytes.items():
        metrics[f"previous_{name}_bytes"] = value
    for name, error in self.errors.items():
        metrics[f"{name}_error"] = error
    return metrics

Functions:

fastvideo.mlx_runtime.memory.add_memory_limit_args

add_memory_limit_args(parser: ArgumentParser, *, mlx_memory_limit_gib: float | None = None, mlx_cache_limit_gib: float | None = None, mlx_disable_cache: bool = False, mlx_wired_limit_gib: float | None = None, torch_mps_high_watermark_ratio: float | None = None, torch_mps_low_watermark_ratio: float | None = None) -> None

Add configurable Apple Silicon memory-limit options to an argument parser.

Parameters:

Name Type Description Default
parser ArgumentParser

Parser to which the options are added.

required
mlx_memory_limit_gib float | None

Default MLX memory limit in GiB.

None
mlx_cache_limit_gib float | None

Default MLX cache limit in GiB.

None
mlx_disable_cache bool

Whether the cache limit defaults to zero.

False
mlx_wired_limit_gib float | None

Default MLX wired-memory limit in GiB.

None
torch_mps_high_watermark_ratio float | None

Default PyTorch MPS high-watermark ratio.

None
torch_mps_low_watermark_ratio float | None

Default PyTorch MPS low-watermark ratio.

None
Source code in fastvideo/mlx_runtime/memory.py
def add_memory_limit_args(
    parser: argparse.ArgumentParser,
    *,
    mlx_memory_limit_gib: float | None = None,
    mlx_cache_limit_gib: float | None = None,
    mlx_disable_cache: bool = False,
    mlx_wired_limit_gib: float | None = None,
    torch_mps_high_watermark_ratio: float | None = None,
    torch_mps_low_watermark_ratio: float | None = None,
) -> None:
    """
    Add configurable Apple Silicon memory-limit options to an argument parser.

    Parameters:
        parser (argparse.ArgumentParser): Parser to which the options are added.
        mlx_memory_limit_gib (float | None): Default MLX memory limit in GiB.
        mlx_cache_limit_gib (float | None): Default MLX cache limit in GiB.
        mlx_disable_cache (bool): Whether the cache limit defaults to zero.
        mlx_wired_limit_gib (float | None): Default MLX wired-memory limit in GiB.
        torch_mps_high_watermark_ratio (float | None): Default PyTorch MPS high-watermark ratio.
        torch_mps_low_watermark_ratio (float | None): Default PyTorch MPS low-watermark ratio.
    """
    parser.add_argument("--mlx-memory-limit-gib",
                        type=float,
                        default=mlx_memory_limit_gib,
                        help="Set MLX memory limit in GiB for memory-tier testing (DiT path).")
    parser.add_argument("--mlx-cache-limit-gib",
                        type=float,
                        default=mlx_cache_limit_gib,
                        help="Set MLX cache limit in GiB. Use --mlx-disable-cache to force 0.")
    parser.add_argument("--mlx-disable-cache",
                        action="store_true",
                        default=mlx_disable_cache,
                        help="Set MLX cache limit to 0 for stricter memory-tier tests.")
    parser.add_argument("--mlx-wired-limit-gib",
                        type=float,
                        default=mlx_wired_limit_gib,
                        help="Set MLX wired-memory limit in GiB where supported by macOS/MLX.")
    parser.add_argument("--torch-mps-high-watermark-ratio",
                        type=float,
                        default=torch_mps_high_watermark_ratio,
                        help="Set PYTORCH_MPS_HIGH_WATERMARK_RATIO before importing torch.")
    parser.add_argument("--torch-mps-low-watermark-ratio",
                        type=float,
                        default=torch_mps_low_watermark_ratio,
                        help="Set PYTORCH_MPS_LOW_WATERMARK_RATIO before importing torch.")

fastvideo.mlx_runtime.memory.apply_memory_limits

apply_memory_limits(*, mlx_memory_limit_gib: float | None = None, mlx_cache_limit_gib: float | None = None, mlx_disable_cache: bool = False, mlx_wired_limit_gib: float | None = None, torch_mps_high_watermark_ratio: float | None = None, torch_mps_low_watermark_ratio: float | None = None, mx_module: Any | None = None) -> AppliedMemoryLimits

Apply optional MLX allocator limits and PyTorch MPS watermarks.

PyTorch reads MPS watermark variables when the MPS backend initializes, so call this before importing PyTorch. Specifying only a high watermark sets the low watermark to 0.0. MLX limit-setting failures are recorded in the result and do not prevent other limits from being applied.

Parameters:

Name Type Description Default
mlx_memory_limit_gib float | None

Maximum MLX memory in GiB.

None
mlx_cache_limit_gib float | None

Maximum MLX cache size in GiB.

None
mlx_disable_cache bool

Whether to disable the MLX cache.

False
mlx_wired_limit_gib float | None

Maximum MLX wired memory in GiB.

None
torch_mps_high_watermark_ratio float | None

PyTorch MPS high watermark ratio.

None
torch_mps_low_watermark_ratio float | None

PyTorch MPS low watermark ratio.

None

Returns:

Name Type Description
AppliedMemoryLimits AppliedMemoryLimits

Configured values, applied and previous MLX byte limits, MPS watermark values, and per-limit errors.

Source code in fastvideo/mlx_runtime/memory.py
def apply_memory_limits(
    *,
    mlx_memory_limit_gib: float | None = None,
    mlx_cache_limit_gib: float | None = None,
    mlx_disable_cache: bool = False,
    mlx_wired_limit_gib: float | None = None,
    torch_mps_high_watermark_ratio: float | None = None,
    torch_mps_low_watermark_ratio: float | None = None,
    mx_module: Any | None = None,
) -> AppliedMemoryLimits:
    """Apply optional MLX allocator limits and PyTorch MPS watermarks.

    PyTorch reads MPS watermark variables when the MPS backend initializes, so
    call this before importing PyTorch. Specifying only a high watermark sets the
    low watermark to ``0.0``. MLX limit-setting failures are recorded in the
    result and do not prevent other limits from being applied.

    Parameters:
        mlx_memory_limit_gib (float | None): Maximum MLX memory in GiB.
        mlx_cache_limit_gib (float | None): Maximum MLX cache size in GiB.
        mlx_disable_cache (bool): Whether to disable the MLX cache.
        mlx_wired_limit_gib (float | None): Maximum MLX wired memory in GiB.
        torch_mps_high_watermark_ratio (float | None): PyTorch MPS high watermark
            ratio.
        torch_mps_low_watermark_ratio (float | None): PyTorch MPS low watermark
            ratio.

    Returns:
        AppliedMemoryLimits: Configured values, applied and previous MLX byte
            limits, MPS watermark values, and per-limit errors.
    """
    if torch_mps_high_watermark_ratio is not None and torch_mps_low_watermark_ratio is None:
        torch_mps_low_watermark_ratio = 0.0

    high = _set_mps_env("PYTORCH_MPS_HIGH_WATERMARK_RATIO", torch_mps_high_watermark_ratio)
    low = _set_mps_env("PYTORCH_MPS_LOW_WATERMARK_RATIO", torch_mps_low_watermark_ratio)

    memory_bytes = gib_to_bytes(mlx_memory_limit_gib)
    cache_bytes = 0 if mlx_disable_cache else gib_to_bytes(mlx_cache_limit_gib)
    wired_bytes = gib_to_bytes(mlx_wired_limit_gib)

    applied: dict[str, int] = {}
    previous: dict[str, int] = {}
    errors: dict[str, str] = {}
    if memory_bytes is not None or cache_bytes is not None or wired_bytes is not None:
        if mx_module is None:
            import mlx.core as mx

            mx_module = mx

        # Apply each limit independently; record failures without stopping.
        limits = [
            ("mlx_memory_limit", memory_bytes, mx_module.set_memory_limit),
            ("mlx_cache_limit", cache_bytes, mx_module.set_cache_limit),
            ("mlx_wired_limit", wired_bytes, mx_module.set_wired_limit),
        ]
        for name, value, setter in limits:
            if value is not None:
                try:
                    previous[name] = int(setter(value))
                    applied[name] = value
                except Exception as exc:  # noqa: BLE001 - macOS/system-limit dependent.
                    errors[name] = f"{type(exc).__name__}: {exc}"

    return AppliedMemoryLimits(
        mlx_memory_limit_gib=mlx_memory_limit_gib,
        mlx_cache_limit_gib=mlx_cache_limit_gib,
        mlx_disable_cache=mlx_disable_cache,
        mlx_wired_limit_gib=mlx_wired_limit_gib,
        torch_mps_high_watermark_ratio=high,
        torch_mps_low_watermark_ratio=low,
        applied_bytes=applied,
        previous_bytes=previous,
        errors=errors,
    )

fastvideo.mlx_runtime.memory.cleanup_mlx

cleanup_mlx(mx_module: Any | None = None) -> None

Collect unreachable MLX objects, then release their allocator cache.

Source code in fastvideo/mlx_runtime/memory.py
def cleanup_mlx(mx_module: Any | None = None) -> None:
    """Collect unreachable MLX objects, then release their allocator cache."""
    if mx_module is None:
        import mlx.core as mx

        mx_module = mx
    gc.collect()
    mx_module.clear_cache()

fastvideo.mlx_runtime.memory.cleanup_torch_mps

cleanup_torch_mps(torch_module: Any | None = None) -> None

Collect unreachable Torch objects, then release the MPS allocator cache.

Source code in fastvideo/mlx_runtime/memory.py
def cleanup_torch_mps(torch_module: Any | None = None) -> None:
    """Collect unreachable Torch objects, then release the MPS allocator cache."""
    if torch_module is None:
        import torch

        torch_module = torch
    gc.collect()
    if torch_module.backends.mps.is_available():
        torch_module.mps.empty_cache()

fastvideo.mlx_runtime.memory.gib_to_bytes

gib_to_bytes(value: float | None) -> int | None

Convert a positive memory limit from GiB to bytes.

Parameters:

Name Type Description Default
value float | None

Memory limit in GiB, or None when unset.

required

Returns:

Type Description
int | None

int | None: The memory limit in bytes, or None when no limit is provided.

Raises:

Type Description
ValueError

If value is zero or negative.

Source code in fastvideo/mlx_runtime/memory.py
def gib_to_bytes(value: float | None) -> int | None:
    """
    Convert a positive memory limit from GiB to bytes.

    Parameters:
        value (float | None): Memory limit in GiB, or `None` when unset.

    Returns:
        int | None: The memory limit in bytes, or `None` when no limit is provided.

    Raises:
        ValueError: If `value` is zero or negative.
    """
    if value is None:
        return None
    if value <= 0:
        raise ValueError(f"Memory limit must be positive GiB, got {value}")
    return int(value * GIB)