mlx-tune: T1 env-gated UNet mx.compile + T3 StaticMoELayer (kills ~4800 .item() syncs/gen, bit-identical, compile-unlocked) + T4 fused DINO SDPA (1.41x micro, parity 1e-7)
Micro gates on m3ultra (BENCHMARKS.md): T1 parity 3.0e-05 @ 1.04x; T3 parity 0.0 (+compiles clean); T4 parity 1.1e-07 @ 1.409x. E2E flag-on-vs-off gen pending GPU slot. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
04fd1ceb28
commit
4b368eef55
@ -54,9 +54,9 @@ class ViTAttention(nn.Module):
|
|||||||
qkv = qkv.transpose(2, 0, 3, 1, 4) # (3, B, H, N, D)
|
qkv = qkv.transpose(2, 0, 3, 1, 4) # (3, B, H, N, D)
|
||||||
q, k, v = qkv[0], qkv[1], qkv[2]
|
q, k, v = qkv[0], qkv[1], qkv[2]
|
||||||
|
|
||||||
attn = (q @ k.transpose(0, 1, 3, 2)) * self.scale
|
# fused SDPA (head_dim 64 = fast path); replaces manual q@k.T softmax
|
||||||
attn = mx.softmax(attn, axis=-1)
|
x = mx.fast.scaled_dot_product_attention(q, k, v, scale=self.scale)
|
||||||
x = (attn @ v).transpose(0, 2, 1, 3).reshape(B, N, C)
|
x = x.transpose(0, 2, 1, 3).reshape(B, N, C)
|
||||||
return self.proj(x)
|
return self.proj(x)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,8 @@ modules attached externally via load_model._enhance_unet.
|
|||||||
|
|
||||||
from typing import Dict, Optional, Tuple
|
from typing import Dict, Optional, Tuple
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
import mlx.core as mx
|
import mlx.core as mx
|
||||||
import mlx.nn as nn
|
import mlx.nn as nn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -161,6 +163,24 @@ class UNet2DConditionModelMLX(nn.Module):
|
|||||||
timestep: mx.array,
|
timestep: mx.array,
|
||||||
encoder_hidden_states: Optional[mx.array] = None,
|
encoder_hidden_states: Optional[mx.array] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
|
) -> mx.array:
|
||||||
|
# HY3D_MLX_COMPILE=1 routes through mx.compile (fixed-shape denoise
|
||||||
|
# loop -> one trace per chunk-shape/ctx-structure; corridorkey
|
||||||
|
# precedent 4c660df). Default off = numerics identical to upstream.
|
||||||
|
if os.environ.get("HY3D_MLX_COMPILE", "0") == "1":
|
||||||
|
fn = getattr(self, "_compiled_forward", None)
|
||||||
|
if fn is None:
|
||||||
|
fn = mx.compile(self._forward)
|
||||||
|
self._compiled_forward = fn
|
||||||
|
return fn(sample, timestep, encoder_hidden_states, **kwargs)
|
||||||
|
return self._forward(sample, timestep, encoder_hidden_states, **kwargs)
|
||||||
|
|
||||||
|
def _forward(
|
||||||
|
self,
|
||||||
|
sample: mx.array,
|
||||||
|
timestep: mx.array,
|
||||||
|
encoder_hidden_states: Optional[mx.array] = None,
|
||||||
|
**kwargs,
|
||||||
) -> mx.array:
|
) -> mx.array:
|
||||||
"""
|
"""
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@ -19,7 +19,8 @@ from typing import Optional
|
|||||||
import mlx.core as mx
|
import mlx.core as mx
|
||||||
import mlx.nn as nn
|
import mlx.nn as nn
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from mlx_arsenal.moe import MoELayer
|
from mlx_arsenal.moe import MoELayer # noqa: F401 (kept for API compat)
|
||||||
|
from .moe_static import StaticMoELayer
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@ -353,7 +354,7 @@ class HunYuanDiTBlock(nn.Module):
|
|||||||
return MLP(hidden_size)
|
return MLP(hidden_size)
|
||||||
|
|
||||||
shared = MLP(hidden_size)
|
shared = MLP(hidden_size)
|
||||||
self.moe = MoELayer(
|
self.moe = StaticMoELayer(
|
||||||
hidden_size=hidden_size,
|
hidden_size=hidden_size,
|
||||||
num_experts=num_experts,
|
num_experts=num_experts,
|
||||||
top_k=moe_top_k,
|
top_k=moe_top_k,
|
||||||
|
|||||||
37
hy3dshape/hy3dshape/models/denoisers/moe_static.py
Normal file
37
hy3dshape/hy3dshape/models/denoisers/moe_static.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
"""Static MoE: mlx_arsenal.MoELayer minus the per-expert `.item()` early-skip.
|
||||||
|
|
||||||
|
The skip costs a host sync per expert per block per step (~4800/gen) and
|
||||||
|
makes the graph dynamic (blocks mx.compile). With top-k routing over
|
||||||
|
thousands of tokens every expert almost always receives work, so the skip
|
||||||
|
almost never fires — running all experts with w=0 rows zeroed is
|
||||||
|
numerically identical (same accumulation order) and fully static.
|
||||||
|
Vendored subclass — mlx_arsenal itself is a SHARED dependency, unmodified.
|
||||||
|
"""
|
||||||
|
import mlx.core as mx
|
||||||
|
|
||||||
|
from mlx_arsenal.moe import MoELayer
|
||||||
|
|
||||||
|
|
||||||
|
class StaticMoELayer(MoELayer):
|
||||||
|
def __call__(self, x: mx.array) -> mx.array:
|
||||||
|
orig_shape = x.shape
|
||||||
|
hidden = x.reshape(-1, orig_shape[-1])
|
||||||
|
indices, weights = self.gate(hidden)
|
||||||
|
num_experts = len(self.experts)
|
||||||
|
num_tokens = hidden.shape[0]
|
||||||
|
expert_weights = mx.zeros((num_tokens, num_experts))
|
||||||
|
for k in range(indices.shape[1]):
|
||||||
|
col_indices = indices[:, k : k + 1]
|
||||||
|
col_weights = weights[:, k : k + 1]
|
||||||
|
one_hot = mx.zeros((num_tokens, num_experts))
|
||||||
|
rows = mx.arange(num_tokens).reshape(-1, 1)
|
||||||
|
one_hot[rows, col_indices] = col_weights
|
||||||
|
expert_weights = expert_weights + one_hot
|
||||||
|
output = mx.zeros_like(hidden)
|
||||||
|
for expert_idx, expert in enumerate(self.experts):
|
||||||
|
w = expert_weights[:, expert_idx : expert_idx + 1]
|
||||||
|
expert_output = expert(hidden)
|
||||||
|
output = output + w * expert_output
|
||||||
|
if self.shared_expert is not None:
|
||||||
|
output = output + self.shared_expert(hidden)
|
||||||
|
return output.reshape(orig_shape)
|
||||||
Loading…
Reference in New Issue
Block a user