Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
cb8867f
feat(platform): add PyTorch MPS device backend
q6y6y6 Aug 31, 2026
9babeeb
fix(mps): isolate CUDA-only imports
q6y6y6 Aug 31, 2026
8e00049
fix(runtime): support MPS cache management
q6y6y6 Aug 31, 2026
a54fbc2
feat(mps): add MiniMax H3 inference config
q6y6y6 Sep 1, 2026
8534b4e
feat(mps): add MiniMax H3 CLI entry and launch script
q6y6y6 Sep 1, 2026
b6a47aa
feat(mps): stream MiniMax H3 Qwen weights from disk
q6y6y6 Sep 1, 2026
a7fb5f5
feat(mps): stream MiniMax H3 DiT weights from disk
q6y6y6 Sep 1, 2026
87dbd18
fix(runtime): make cache cleanup device-aware
q6y6y6 Sep 1, 2026
0fb815c
fix(mps): support MPS random seeding
q6y6y6 Sep 1, 2026
c2a3d01
feat(mps): add MiniMax H3 low-memory lifecycle
q6y6y6 Sep 1, 2026
f6d1845
feat(mps): enable MiniMax H3 low-memory t2av
q6y6y6 Sep 2, 2026
7fe6a7d
fix(minimax_h3): support official video VAE checkpoint layout
q6y6y6 Sep 2, 2026
a02eae8
fix(minimax_h3): load official video VAE checkpoints
q6y6y6 Sep 4, 2026
8bbf600
fix(minimax_h3): fix Qwen disk streaming dispatch
q6y6y6 Sep 5, 2026
fd0a91f
feat(minimax_h3): load official DiT checkpoints
q6y6y6 Sep 5, 2026
44581ec
fix(minimax_h3): cast packed positions before device transfer
q6y6y6 Sep 5, 2026
1173221
fix(minimax_h3): release Audio VAE weight norm cache
q6y6y6 Sep 5, 2026
ad3a7af
fix(mps): chunk MiniMax H3 SDPA queries
q6y6y6 Sep 5, 2026
4c82ebb
fix(minimax_h3): match reference RoPE precision
q6y6y6 Sep 6, 2026
6378266
feat(minimax_h3): support LoRA with disk streaming
q6y6y6 Sep 6, 2026
3a65e51
feat(mps): add MiniMax H3 Turbo 4-step config
q6y6y6 Sep 7, 2026
f36f5b8
fix(minimax_h3): correct Video VAE QKV weight mapping
q6y6y6 Sep 7, 2026
64f5194
fix(mps): avoid broken temporal padding in H3 VAE
q6y6y6 Sep 7, 2026
bb918d1
fix(minimax_h3): support short clip inference for H3
q6y6y6 Sep 8, 2026
2251957
Avoid redundant tensor clone in MiniMax-H3 streaming loader
q6y6y6 Sep 8, 2026
29e1be7
Merge origin/main into feat/mps-support
q6y6y6 Sep 10, 2026
7be702c
Merge remote-tracking branch 'upstream/main' into feat/mps-support
helloyongyang Sep 10, 2026
0197365
style: format MiniMax-H3 PR files
q6y6y6 Sep 10, 2026
2e5219a
Merge remote-tracking branch 'myfork/feat/mps-support' into feat/mps-…
q6y6y6 Sep 10, 2026
81e0c53
Merge upstream/main into feat/mps-support
q6y6y6 Sep 10, 2026
83491d7
fix(minimax_h3): support raw checkpoints in AdaLN cache builder
q6y6y6 Sep 10, 2026
31ac227
perf(minimax_h3): optimize streaming chunk size by target bytes
q6y6y6 Sep 10, 2026
79cb146
chore(minimax_h3): remove default Turbo LoRA config
q6y6y6 Sep 10, 2026
62e29b5
refactor(minimax_h3)!: align checkpoint loading with upstream
helloyongyang Sep 10, 2026
f502c4a
perf(mps): 为 MiniMax-H3 实现共享内存双 buffer offload
helloyongyang Sep 10, 2026
97d0f06
Merge upstream/main into feat/mps-support
helloyongyang Sep 10, 2026
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
51 changes: 51 additions & 0 deletions configs/platforms/mps/minimax_h3_t2av.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"infer_steps": 29,
"num_frames": 124,
"size": [480, 480],
"fps": 24,
"enable_cfg": false,

"cpu_offload": true,
"offload_granularity": "block",
"dit_prepost_resident": false,
"dit_disk_streaming": true,
"dit_mps_shared_buffer": true,
"use_adaln_cache": true,
"adaln_cache_dir": "~/.cache/lightx2v/adaln/diffusers",

"text_encoder_cpu_offload": true,
"text_encoder_offload_granularity": "block",
"text_encoder_disk_streaming": true,
"text_encoder_host_pinned": false,
"text_encoder_release_block_offload_buffers": true,
"text_encoder_quantized": false,

"vae_cpu_offload": true,
"vae_use_compile": false,
"vae_attn_type": "torch_sdpa",
"video_vae_quantized": false,

"lazy_load": false,
"unload_modules": false,
"warmup": false,

"attn_type": "torch_sdpa",
"mps_sdpa_query_chunk_size": 512,
"rms_type": "torch_native",
"rope_type": "torch_real_rope",

"tensor_parallel": false,
"dit_quantized": false,
"dit_quant_scheme": "Default",

"feature_caching": "NoCaching",
"use_compile": false,

"video_flow_shift": 12.0,
"audio_flow_shift": 3.0,
"vae_spatial_scale_factor": 16,
"audio_sampling_rate": 32000,
"audio_latents_per_second": 40,
"audio_channels": 2,
"keep_latents_dtype_in_scheduler": true
}
51 changes: 51 additions & 0 deletions configs/platforms/mps/minimax_h3_t2av_4step_512_22.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"infer_steps": 4,
"num_frames": 22,
"size": [512, 512],
"fps": 24,
"enable_cfg": false,

"cpu_offload": true,
"offload_granularity": "block",
"dit_prepost_resident": false,
"dit_disk_streaming": true,
"dit_mps_shared_buffer": true,
"use_adaln_cache": true,
"adaln_cache_dir": "~/.cache/lightx2v/adaln/diffusers",

"text_encoder_cpu_offload": true,
"text_encoder_offload_granularity": "block",
"text_encoder_disk_streaming": true,
"text_encoder_host_pinned": false,
"text_encoder_release_block_offload_buffers": true,
"text_encoder_quantized": false,

"vae_cpu_offload": true,
"vae_use_compile": false,
"vae_attn_type": "torch_sdpa",
"video_vae_quantized": false,

"lazy_load": false,
"unload_modules": false,
"warmup": false,

"attn_type": "torch_sdpa",
"mps_sdpa_query_chunk_size": 512,
"rms_type": "torch_native",
"rope_type": "torch_real_rope",

"tensor_parallel": false,
"dit_quantized": false,
"dit_quant_scheme": "Default",

"feature_caching": "NoCaching",
"use_compile": false,

"video_flow_shift": 12.0,
"audio_flow_shift": 3.0,
"vae_spatial_scale_factor": 16,
"audio_sampling_rate": 32000,
"audio_latents_per_second": 40,
"audio_channels": 2,
"keep_latents_dtype_in_scheduler": true
}
14 changes: 13 additions & 1 deletion lightx2v/common/offload/manager.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from concurrent.futures import ThreadPoolExecutor
from contextlib import nullcontext

import torch
from loguru import logger
Expand All @@ -14,9 +15,12 @@
class WeightAsyncStreamManager(object):
def __init__(self, offload_granularity):
self.offload_granularity = offload_granularity
self.init_stream = torch_device_module.Stream(priority=0)
self.need_init_first_buffer = True
self.lazy_load = False
self._init_streams()

def _init_streams(self):
self.init_stream = torch_device_module.Stream(priority=0)
torch_version = parse(torch.__version__.split("+")[0])
# Legacy name: this is the active device backend's weight-loading stream, not a CUDA-only stream.
if AI_DEVICE == "cuda" and torch_version >= parse("2.7"):
Expand All @@ -26,6 +30,14 @@ def __init__(self, offload_granularity):
self.cuda_load_stream = torch_device_module.Stream(priority=0)
self.compute_stream = torch_device_module.Stream(priority=-1)

def prepare_compute(self):
self.compute_stream.wait_stream(torch_device_module.current_stream())

def compute_context(self):
if AI_DEVICE == "xpu":
return nullcontext()
return torch_device_module.stream(self.compute_stream)

def init_cpu_buffer(self, blocks_cpu_buffer=None, phases_cpu_buffer=None):
self.need_init_first_buffer = True
if self.offload_granularity == "block":
Expand Down
89 changes: 89 additions & 0 deletions lightx2v/common/offload/mps_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
"""Double-buffered disk offload into CPU-visible MPS weight storage."""

from contextlib import nullcontext

import torch
from loguru import logger

from lightx2v.common.offload.manager import WeightAsyncStreamManager


def host_view(tensor):
"""Alias MPS storage without copying; the caller orders CPU/GPU accesses."""
if tensor.device.type != "mps":
raise ValueError("Shared weight buffers require an MPS tensor")
if not hasattr(torch.mps, "_host_alias_storage"):
raise RuntimeError("Shared MPS weight buffers require torch.mps._host_alias_storage (PyTorch >= 2.13)")
storage = torch.mps._host_alias_storage(tensor.untyped_storage())
return torch.empty(0, dtype=tensor.dtype).set_(storage, tensor.storage_offset(), tensor.shape, tensor.stride())


class MpsSharedWeightAsyncStreamManager(WeightAsyncStreamManager):
"""Read into the idle slot while the default MPS stream uses the other.

The source implements load_block_into(target_buffer, block_idx). This runs
on a CPU worker and must only write host views of the supplied idle buffer.
"""

def __init__(self, offload_granularity="block"):
if offload_granularity != "block":
raise ValueError("Shared MPS weight offload only supports block granularity")
super().__init__(offload_granularity)
self.cuda_buffers = []
self.init_lazy_load(num_workers=1)
logger.info("MPS shared weight offload: two device buffers, direct file reads, one prefetch worker")

def _init_streams(self):
# Disk I/O runs on a CPU thread; GPU work stays on the default stream.
pass

def prepare_compute(self):
pass

def compute_context(self):
return nullcontext()

def init_cuda_buffer(self, blocks_cuda_buffer=None, phases_cuda_buffer=None):
if blocks_cuda_buffer is None or len(blocks_cuda_buffer) != 2:
raise ValueError("Shared MPS offload requires exactly two device buffers")
if self.prefetch_futures:
raise RuntimeError("Cannot replace shared buffers while a prefetch is pending")
super().init_cuda_buffer(blocks_cuda_buffer, phases_cuda_buffer)
if self.executor is None:
self.init_lazy_load(num_workers=1)

def init_first_buffer(self, blocks, adapter_block_idx=None):
torch.mps.synchronize()
blocks.load_block_into(self.cuda_buffers[0], 0)
torch.mps.synchronize()
self.need_init_first_buffer = False

def prefetch_weights(self, block_idx, blocks, adapter_block_idx=None):
if self.prefetch_futures:
raise RuntimeError("Call swap_blocks before scheduling another prefetch")
self.prefetch_block_idx = block_idx
self.prefetch_futures = [self.executor.submit(blocks.load_block_into, self.cuda_buffers[1], block_idx)]

def swap_blocks(self):
if not self.prefetch_futures:
raise RuntimeError("No shared-buffer prefetch to complete")
# This commits GPU work and releases the GIL while waiting, so the disk
# reader continues filling slot 1 while the GPU consumes slot 0.
torch.mps.synchronize()
for future in self.prefetch_futures:
future.result()
torch.mps.synchronize()
self.prefetch_futures.clear()
self.cuda_buffers.reverse()

def close(self):
"""Drain both users before releasing aliases; allow later reinitialization."""
try:
if self.executor is not None:
self.executor.shutdown(wait=True)
torch.mps.synchronize()
finally:
self.executor = None
self.prefetch_futures.clear()
self.cuda_buffers = []
self.need_init_first_buffer = True
46 changes: 25 additions & 21 deletions lightx2v/common/ops/attn/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
from .draft_attn import DraftAttnWeight
from .dynamic_sparse_attn import DynamicSparseAttnWeight
from .flash_attn import (
FlashAttn2Weight,
FlashAttn3Weight,
FlashAttn4Weight,
SparseFlashAttn4Weight,
)
from .general_sparse_attn import GeneralSparseAttnWeight
from .nbhd_attn import NbhdAttnWeight, NbhdAttnWeightFlashInfer
from .radial_attn import RadialAttnWeight
from .rainfusion_attn import RainfusionAttnWeight
from .ring_attn import RingAttnWeight
from .sage_attn import SageAttn2KInt8VFP8Weight, SageAttn2Weight, SageAttn3Weight, SparseSageAttn2Weight, SparseSageAttn3Weight
from .sol_attn import SolAttnWeight
from .sparge_attn import SpargeAttnWeight
from .sparse_mask_generator import NbhdMaskGenerator, SlaMaskGenerator, SpargeMaskGenerator, SvgMaskGenerator
from .sparse_operator import FlashinferOperator, FlexBlockOperator, MagiOperator, SlaTritonOperator, SparseFlashAttentionV4Operator, SparseSageAttentionV2Operator, SparseSageAttentionV3Operator
from .svg2_attn import Svg2AttnWeight
from .svg_attn import SvgAttnWeight
from lightx2v_platform.base.global_var import AI_DEVICE

from .torch_sdpa import TorchSDPAWeight
from .ulysses_attn import UlyssesAttnWeight

if str(AI_DEVICE) != "mps":
from .draft_attn import DraftAttnWeight
from .dynamic_sparse_attn import DynamicSparseAttnWeight
from .flash_attn import (
FlashAttn2Weight,
FlashAttn3Weight,
FlashAttn4Weight,
SparseFlashAttn4Weight,
)
from .general_sparse_attn import GeneralSparseAttnWeight
from .nbhd_attn import NbhdAttnWeight, NbhdAttnWeightFlashInfer
from .radial_attn import RadialAttnWeight
from .rainfusion_attn import RainfusionAttnWeight
from .ring_attn import RingAttnWeight
from .sage_attn import SageAttn2KInt8VFP8Weight, SageAttn2Weight, SageAttn3Weight, SparseSageAttn2Weight, SparseSageAttn3Weight
from .sol_attn import SolAttnWeight
from .sparge_attn import SpargeAttnWeight
from .sparse_mask_generator import NbhdMaskGenerator, SlaMaskGenerator, SpargeMaskGenerator, SvgMaskGenerator
from .sparse_operator import FlashinferOperator, FlexBlockOperator, MagiOperator, SlaTritonOperator, SparseFlashAttentionV4Operator, SparseSageAttentionV2Operator, SparseSageAttentionV3Operator
from .svg2_attn import Svg2AttnWeight
from .svg_attn import SvgAttnWeight
from .ulysses_attn import UlyssesAttnWeight
53 changes: 42 additions & 11 deletions lightx2v/common/ops/attn/torch_sdpa.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,33 @@
from .template import AttnWeightTemplate


def _use_h3_mps_query_chunks(q, k, v, chunk_size, scope, attn_mask, causal, drop_rate):
return (
scope == "minimax_h3_dit"
and isinstance(chunk_size, int)
and chunk_size > 0
and q.device.type == k.device.type == v.device.type == "mps"
and q.ndim == 4
and q.shape == k.shape == v.shape
and q.shape[0] == 1
and q.shape[1] == 56
and q.shape[-1] == 128
and q.shape[2] > 0
and attn_mask is None
and not causal
and drop_rate == 0
)


def _query_chunked_sdpa(q, k, v, chunk_size):
# Each query still attends to every key/value. Only the query workspace is
# bounded; there is no context truncation or change to the softmax domain.
return torch.cat(
[F.scaled_dot_product_attention(q[:, :, start : start + chunk_size, :], k, v, attn_mask=None, dropout_p=0.0, is_causal=False) for start in range(0, q.shape[2], chunk_size)],
dim=2,
)


@ATTN_WEIGHT_REGISTER("torch_sdpa")
class TorchSDPAWeight(AttnWeightTemplate):
def __init__(self):
Expand Down Expand Up @@ -44,17 +71,21 @@ def apply(
enable_mem_efficient=True,
)
with sdpa_ctx:
# q/k/v are (B, H, S, D) here, so head count is dim 1. GQA models such as
# neopp (32 q heads, 8 kv heads) need SDPA to broadcast the kv groups.
x = F.scaled_dot_product_attention(
q,
k,
v,
attn_mask=attn_mask,
dropout_p=drop_rate,
is_causal=causal,
enable_gqa=q.shape[1] != k.shape[1],
)
chunk_size = kwargs.get("mps_sdpa_query_chunk_size", 0)
if _use_h3_mps_query_chunks(q, k, v, chunk_size, kwargs.get("attention_scope"), attn_mask, causal, drop_rate):
x = _query_chunked_sdpa(q, k, v, chunk_size)
else:
# q/k/v are (B, H, S, D) here, so head count is dim 1. GQA models such as
# neopp (32 q heads, 8 kv heads) need SDPA to broadcast the kv groups.
x = F.scaled_dot_product_attention(
q,
k,
v,
attn_mask=attn_mask,
dropout_p=drop_rate,
is_causal=causal,
enable_gqa=q.shape[1] != k.shape[1],
)
x = x.transpose(1, 2)
b, s, a, d = x.shape
out = x.reshape(b, s, -1)
Expand Down
4 changes: 2 additions & 2 deletions lightx2v/common/ops/mm/fp8_f16_accum.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

import torch

from lightx2v.common.ops.mm.triton_kernels import fp8_quantize_range_triton

try:
from lightx2v_kernel.gemm import FP8_F16_ACCUM_MM_AVAILABLE, cutlass_scaled_fp8_mm_f16_accum
except ImportError:
Expand Down Expand Up @@ -35,6 +33,8 @@ def validate_fp8_f16_accum_qmax(activation_qmax):


def fp8_f16_accum_linear(input_tensor, weight, weight_scale, bias, activation_qmax):
from lightx2v.common.ops.mm.triton_kernels import fp8_quantize_range_triton

input_shape = input_tensor.shape
input_matrix = input_tensor.reshape(-1, input_shape[-1])
quantized, activation_scale = fp8_quantize_range_triton(input_matrix, activation_qmax)
Expand Down
Loading
Loading