Speed up MOSS-Image edit kv_cache training: reference isolation as unmasked flash attention instead of a boolean mask

This commit is contained in:
Jaret Burkett committed 2026-10-17 12:10:31 -06:00
1 parent 9a1828f201
commit 88360c68b2
1 file changed
+36 -12
@@ -64,7 +64,9 @@ def _rotate(x, rope):
return x * cos + torch.cat((-b, a), -1) * sin return x * cos + torch.cat((-b, a), -1) * sin
def _attention(q, k, v, rope, heads, mask, ref_span=None, kv_capture=None, kv_cache=None): def _attention(
q, k, v, rope, heads, mask, ref_span=None, kv_capture=None, kv_cache=None, isolate_split=None
):
shape = (*q.shape[:2], heads, q.shape[-1] // heads) shape = (*q.shape[:2], heads, q.shape[-1] // heads)
q = _rotate(q.reshape(shape), rope).transpose(1, 2) q = _rotate(q.reshape(shape), rope).transpose(1, 2)
k = _rotate(k.reshape(shape), rope).transpose(1, 2) k = _rotate(k.reshape(shape), rope).transpose(1, 2)
@@ -82,7 +84,18 @@ def _attention(q, k, v, rope, heads, mask, ref_span=None, kv_capture=None, kv_ca
if kv_cache is not None: if kv_cache is not None:
k = torch.cat((k, kv_cache[0].to(k.dtype)), 2) k = torch.cat((k, kv_cache[0].to(k.dtype)), 2)
v = torch.cat((v, kv_cache[1].to(v.dtype)), 2) v = torch.cat((v, kv_cache[1].to(v.dtype)), 2)
out = F.scaled_dot_product_attention(q.to(v.dtype), k.to(v.dtype), v, attn_mask=mask) q, k = q.to(v.dtype), k.to(v.dtype)
if isolate_split is not None:
# reference isolation without a mask (a boolean mask forces SDPA off
# flash attention; its backward is ~4x slower): live queries attend
# every key, reference queries only reference keys
live = F.scaled_dot_product_attention(q[:, :, :isolate_split], k, v)
ref = F.scaled_dot_product_attention(
q[:, :, isolate_split:], k[:, :, isolate_split:], v[:, :, isolate_split:]
)
out = torch.cat((live, ref), 2)
else:
out = F.scaled_dot_product_attention(q, k, v, attn_mask=mask)
return out.transpose(1, 2).flatten(2) return out.transpose(1, 2).flatten(2)
@@ -130,7 +143,9 @@ class _Block(nn.Module):
} }
) )
def forward(self, x, rope, modulation, mask, ref_span=None, kv_capture=None, kv_cache=None): def forward(
self, x, rope, modulation, mask, isolate_split=None, ref_span=None, kv_capture=None, kv_cache=None
):
# modulation (B, 6, D) [shift_a, scale_a, gate_a, shift_f, scale_f, # modulation (B, 6, D) [shift_a, scale_a, gate_a, shift_f, scale_f,
# gate_f], or (mod, ref_mod, split): tokens [split:] are clean # gate_f], or (mod, ref_mod, split): tokens [split:] are clean
# references modulated at t=0 # references modulated at t=0
@@ -165,7 +180,9 @@ class _Block(nn.Module):
q, k, v = self.proj["qkv"](y).chunk(3, -1) q, k, v = self.proj["qkv"](y).chunk(3, -1)
q = self.q_norm(q.unflatten(-1, (self.heads, -1))).flatten(2) q = self.q_norm(q.unflatten(-1, (self.heads, -1))).flatten(2)
k = self.k_norm(k.unflatten(-1, (self.heads, -1))).flatten(2) k = self.k_norm(k.unflatten(-1, (self.heads, -1))).flatten(2)
attn = _attention(q, k, v, rope, self.heads, mask, ref_span, kv_capture, kv_cache) attn = _attention(
q, k, v, rope, self.heads, mask, ref_span, kv_capture, kv_cache, isolate_split
)
x = x + gate(self.proj["o"](attn), 2) x = x + gate(self.proj["o"](attn), 2)
y = affine(self.norm2(x), 4, 3) y = affine(self.norm2(x), 4, 3)
hidden, ffn_gate = self.proj["ffn_up_gate"](y).chunk(2, -1) hidden, ffn_gate = self.proj["ffn_up_gate"](y).chunk(2, -1)
@@ -418,14 +435,18 @@ class MossImageTransformer(nn.Module, OstrisModelMixin):
live_valid = torch.cat( live_valid = torch.cat(
(torch.ones(batch, num_image, dtype=torch.bool, device=device), valid), 1 (torch.ones(batch, num_image, dtype=torch.bool, device=device), valid), 1
) )
joint_mask = None joint_mask = isolate_split = None
if num_ref and isolate_refs: if num_ref and isolate_refs:
key_valid = torch.cat((live_valid, ref_valid), 1) key_valid = torch.cat((live_valid, ref_valid), 1)
is_ref = torch.arange(num_live + num_ref, device=device) >= num_live if bool(key_valid.all()):
allowed = key_valid[:, None, :] & (~is_ref[:, None] | is_ref[None, :])[None] # no padding anywhere: isolation as two unmasked flash calls
# every query keeps at least itself (padded rows would be all-masked) isolate_split = num_live
allowed = allowed | torch.eye(num_live + num_ref, dtype=torch.bool, device=device) else:
joint_mask = allowed[:, None] is_ref = torch.arange(num_live + num_ref, device=device) >= num_live
allowed = key_valid[:, None, :] & (~is_ref[:, None] | is_ref[None, :])[None]
# every query keeps at least itself (padded rows would be all-masked)
allowed = allowed | torch.eye(num_live + num_ref, dtype=torch.bool, device=device)
joint_mask = allowed[:, None]
elif num_ref or reuse_refs: elif num_ref or reuse_refs:
# live queries see every real reference key (cached ones included) # live queries see every real reference key (cached ones included)
key_valid = torch.cat((live_valid, ref_valid), 1) key_valid = torch.cat((live_valid, ref_valid), 1)
@@ -453,12 +474,15 @@ class MossImageTransformer(nn.Module, OstrisModelMixin):
continue continue
if capture: if capture:
slot = [] slot = []
x = layer(x, rope, modulation, joint_mask, (num_live, num_live + num_ref), slot) x = layer(
x, rope, modulation, joint_mask, isolate_split,
(num_live, num_live + num_ref), slot,
)
captured[index] = slot[0] captured[index] = slot[0]
elif reuse_refs: elif reuse_refs:
x = layer(x, rope, modulation, joint_mask, kv_cache=ref_kv_cache["kv"][index]) x = layer(x, rope, modulation, joint_mask, kv_cache=ref_kv_cache["kv"][index])
else: else:
x = self._ckpt(layer, x, rope, modulation, joint_mask) x = self._ckpt(layer, x, rope, modulation, joint_mask, isolate_split)
if capture: if capture:
ref_kv_cache["kv"] = captured ref_kv_cache["kv"] = captured
ref_kv_cache["mask"] = ref_valid ref_kv_cache["mask"] = ref_valid