Skip to content

Commit 0c88a0d

Browse files
Refactor optimizer implementations and improve multi_tensor ops
- Refactor multi_tensor_adam_fl: remove unused inv_scale and out_dtype params - Refactor multi_tensor_adam_param_remainder_fl: use bit manipulation for BF16/int16 FP32 reconstruction matching CUDA implementation exactly - Improve multi_tensor_l2_norm_fl and multi_tensor_scale_fl: add type hints, inf/nan checks, and replace raw operators with flag_gems.mul/flag_gems.add - Update reference optimizer impl to match new signatures and behavior - Export multi_tensor_scale and multi_tensor_l2norm in pytorch optimizers init - Fix missing newline at end of files
1 parent 12b2077 commit 0c88a0d

4 files changed

Lines changed: 256 additions & 144 deletions

File tree

transformer_engine/plugin/core/backends/flagos/impl/fused_adam.py

Lines changed: 59 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
#
33
# See LICENSE for license information.
44

5-
from typing import Optional, List
5+
from typing import List
66
import torch
77
import flag_gems
88

99

10+
11+
1012
def multi_tensor_adam_fl(
1113
chunk_size: int,
1214
noop_flag: torch.Tensor,
@@ -19,8 +21,6 @@ def multi_tensor_adam_fl(
1921
mode: int,
2022
bias_correction: int,
2123
weight_decay: float,
22-
inv_scale: Optional[float] = 1.0,
23-
out_dtype: Optional[torch.dtype] = None,
2424
) -> None:
2525

2626
num_lists = len(tensor_lists)
@@ -50,9 +50,6 @@ def multi_tensor_adam_fl(
5050
if not g.is_contiguous():
5151
g = g.contiguous()
5252

53-
if inv_scale is not None and inv_scale != 1.0:
54-
g = flag_gems.mul(g, inv_scale)
55-
5653
m = flag_gems.add_(flag_gems.mul_(m, beta1), g, alpha=1-beta1)
5754
v = flag_gems.add_(flag_gems.mul_(v, beta2), flag_gems.mul_(flag_gems.mul_(g, g), 1 - beta2))
5855

@@ -73,9 +70,6 @@ def multi_tensor_adam_fl(
7370

7471
if p_master is not None:
7572
flag_gems.copy_(p_master, p)
76-
out_dtype = p_master.dtype if out_dtype is None else out_dtype
77-
p.data = p.data.to(out_dtype)
78-
7973

8074
def multi_tensor_adam_param_remainder_fl(
8175
chunk_size: int,
@@ -89,27 +83,9 @@ def multi_tensor_adam_param_remainder_fl(
8983
mode: int,
9084
bias_correction: int,
9185
weight_decay: float,
92-
inv_scale: Optional[float] = 1.0,
9386
) -> None:
9487
"""
9588
Adam optimizer with parameter remainders for BF16 precision (FlagOS implementation).
96-
97-
This variant stores BF16 parameters + int16 remainders to reconstruct FP32 master weights.
98-
Used when you have BF16 params and need FP32 master params without storing full FP32 copies.
99-
100-
Args:
101-
chunk_size: Chunk size for processing (unused in this implementation)
102-
noop_flag: If non-zero, skip computation
103-
tensor_lists: [grads, params (bf16), exp_avgs (fp32), exp_avg_sqs (fp32), param_remainders (int16)]
104-
lr: Learning rate
105-
beta1: First moment decay rate
106-
beta2: Second moment decay rate
107-
eps: Epsilon for numerical stability
108-
step: Current optimization step
109-
mode: 0 = L2 regularization, 1 = AdamW (decoupled weight decay)
110-
bias_correction: Whether to apply bias correction (1 = yes, 0 = no)
111-
weight_decay: Weight decay coefficient
112-
inv_scale: Inverse gradient scale for mixed precision training
11389
"""
11490
if noop_flag.item() != 0:
11591
return
@@ -133,58 +109,78 @@ def multi_tensor_adam_param_remainder_fl(
133109

134110
for i in range(num_tensors):
135111
g = tensor_lists[0][i]
136-
p = tensor_lists[1][i] # BF16 parameter
112+
p = tensor_lists[1][i] # int16 parameter (high 16 bits of FP32)
137113
m = tensor_lists[2][i] # FP32 first moment
138114
v = tensor_lists[3][i] # FP32 second moment
139-
p_remainder = tensor_lists[4][i] # int16 remainder
115+
p_remainder = tensor_lists[4][i] # int16 remainder (low 16 bits of FP32)
140116

141117
if not g.is_contiguous():
142118
g = g.contiguous()
143119

144-
# Apply gradient unscaling if needed
145-
if inv_scale is not None and inv_scale != 1.0:
146-
g = flag_gems.mul(g, inv_scale)
120+
# Convert gradient to float
121+
g_float = g.float()
122+
123+
# Reconstruct FP32 master weight from int16 param + int16 remainder using bit manipulation
124+
# This matches the CUDA implementation exactly:
125+
# 1. If p_remainder < 0, decrement p (undo rounding)
126+
# 2. Combine high 16 bits (p) and low 16 bits (p_remainder) into FP32
127+
# Note: Use PyTorch native ops for bit manipulation (int16/int32 operations)
128+
129+
local_p = p.view(torch.int16).clone()
130+
local_p_rem = p_remainder.clone()
131+
132+
# Undo rounding: if remainder < 0, decrement p
133+
local_p = torch.where(local_p_rem < 0, local_p - 1, local_p)
134+
135+
# Combine into FP32 using bit shift operations
136+
# local_p is high 16 bits, local_p_rem is low 16 bits
137+
high_bits = local_p.to(torch.int32) << 16
138+
low_bits = local_p_rem.to(torch.int32) & 0xFFFF # Mask off sign extension
139+
param_int32 = high_bits | low_bits
140+
param_master = param_int32.view(torch.float32)
147141

148-
# Reconstruct FP32 master weight from BF16 param + int16 remainder
149-
# The remainder represents the lower 16 bits lost in BF16 conversion
150-
param_fp32 = p.float()
151-
param_master = flag_gems.add(param_fp32, flag_gems.mul(p_remainder.float(), 2.0 ** -16))
142+
# L2 mode: add weight decay to gradient before updating moments
143+
if not is_adamw and weight_decay != 0:
144+
g_float = flag_gems.add(g_float, param_master, alpha=weight_decay)
152145

153-
# Compute gradient with weight decay (if L2 mode)
154-
grad_with_decay = g.float()
155-
if not is_adamw: # L2 regularization mode
156-
grad_with_decay = flag_gems.add(grad_with_decay, flag_gems.mul(param_master, weight_decay))
146+
# Update first moment: m = beta1 * m + (1 - beta1) * g
147+
flag_gems.add_(flag_gems.mul_(m, beta1), g_float, alpha=1 - beta1)
157148

158-
# Update moments
159-
m = flag_gems.add_(flag_gems.mul_(m, beta1), grad_with_decay, alpha=1 - beta1)
160-
v = flag_gems.add_(flag_gems.mul_(v, beta2), flag_gems.mul_(flag_gems.mul_(grad_with_decay, grad_with_decay), 1 - beta2))
149+
# Update second moment: v = beta2 * v + (1 - beta2) * g^2
150+
flag_gems.add_(flag_gems.mul_(v, beta2), flag_gems.mul(g_float, g_float), alpha=1 - beta2)
161151

162152
# Apply bias correction
163-
m_corr = m.clone()
164-
v_corr = v.clone()
165-
if bias_correction == 1:
166-
m_corr = flag_gems.true_divide(m_corr, bias_correction1)
167-
v_corr = flag_gems.true_divide(v_corr, bias_correction2)
153+
m_corr = flag_gems.true_divide(m, bias_correction1)
154+
v_corr = flag_gems.true_divide(v, bias_correction2)
155+
156+
# Compute denominator: sqrt(v_corr) + eps
157+
denom = flag_gems.add(flag_gems.sqrt(v_corr), eps)
168158

169159
# Compute update
170-
update = flag_gems.true_divide(m_corr, flag_gems.add(flag_gems.sqrt(v_corr), eps))
160+
update = flag_gems.true_divide(m_corr, denom)
171161

172-
# Apply weight decay (if AdamW mode)
173-
if is_adamw:
174-
param_master = flag_gems.mul_(param_master, 1 - lr * weight_decay)
162+
# AdamW mode: add decoupled weight decay to update
163+
if is_adamw and weight_decay != 0:
164+
update = flag_gems.add(update, param_master, alpha=weight_decay)
165+
166+
# Update master weight: p = p - lr * update
167+
param_master = flag_gems.sub(param_master, flag_gems.mul(update, lr))
175168

176-
# Update master weight
177-
param_master = flag_gems.add_(param_master, update, alpha=-lr)
169+
# Split FP32 back into int16 param + int16 remainder using bit manipulation
170+
# This matches the CUDA implementation exactly:
171+
# 1. Extract high 16 bits as p
172+
# 2. Extract low 16 bits as p_remainder
173+
# 3. If p_remainder < 0, increment p (round up)
174+
# Note: Use PyTorch native ops for bit manipulation (int32 operations)
178175

179-
# Split back into BF16 param + int16 remainder
180-
# Convert to BF16 (this is the rounded version)
181-
param_bf16 = param_master.to(dtype=p.dtype)
176+
param_int32 = param_master.view(torch.int32)
177+
# Extract low 16 bits (remainder) and high 16 bits (param)
178+
new_p_rem = (param_int32 & 0xFFFF).to(torch.int16)
179+
new_p = ((param_int32 >> 16) & 0xFFFF).to(torch.int16)
182180

183-
# Compute remainder: difference between FP32 master and BF16 representation
184-
# Scale and quantize to int16 range
185-
remainder_fp32 = flag_gems.mul(flag_gems.sub(param_master, param_bf16.float()), 2.0 ** 16)
186-
remainder_int16 = flag_gems.clamp(torch.round(remainder_fp32), -32768, 32767).to(dtype=torch.int16)
181+
# Round up: if remainder < 0, increment p
182+
new_p = torch.where(new_p_rem < 0, new_p + 1, new_p)
187183

188184
# Write back
189-
flag_gems.copy_(p, param_bf16)
190-
flag_gems.copy_(p_remainder, remainder_int16)
185+
flag_gems.copy_(p, new_p.view(torch.bfloat16))
186+
flag_gems.copy_(p_remainder, new_p_rem)

transformer_engine/plugin/core/backends/flagos/impl/multi_tensor.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,66 @@
22
#
33
# See LICENSE for license information.
44

5+
from typing import List, Tuple
56
import torch
6-
from torch.distributed._tensor import DTensor
77
import flag_gems
88

99

10-
def multi_tensor_l2_norm_fl(chunk_size, noop_flag, tensor_lists, per_tensor, *args):
10+
def multi_tensor_l2_norm_fl(
11+
_chunk_size: int,
12+
noop_flag: torch.Tensor,
13+
tensor_lists: List[List[torch.Tensor]],
14+
per_tensor: bool = False,
15+
) -> Tuple[torch.Tensor, torch.Tensor]:
16+
"""
17+
Compute L2 norm of tensors using flag_gems.
18+
19+
Returns:
20+
Tuple of (total_norm, per_tensor_norms_or_dummy)
21+
- total_norm: The combined L2 norm of all tensors
22+
- per_tensor_norms_or_dummy: Per-tensor norms stacked if per_tensor=True, else dummy tensor
23+
"""
24+
device = tensor_lists[0][0].device if tensor_lists and tensor_lists[0] else 'cpu'
25+
26+
if noop_flag.item() != 0:
27+
return torch.tensor(0.0, device=device), torch.tensor(0.0, device=device)
1128

1229
tensors = tensor_lists[0]
1330

31+
# Compute per-tensor norms
32+
per_tensor_norms = []
33+
total_norm_sq = torch.tensor(0.0, device=device)
34+
35+
for tensor in tensors:
36+
t_float = tensor.float()
37+
norm_sq = flag_gems.sum(flag_gems.mul(t_float, t_float))
38+
# Check for inf/nan (matches CUDA behavior)
39+
if not torch.isfinite(norm_sq):
40+
noop_flag.fill_(1)
41+
total_norm_sq = flag_gems.add(total_norm_sq, norm_sq)
42+
if per_tensor:
43+
per_tensor_norms.append(flag_gems.sqrt(norm_sq))
44+
45+
total_norm = flag_gems.sqrt(total_norm_sq)
46+
1447
if per_tensor:
15-
norms = [torch.norm(t.float(), p=2) for t in tensors]
16-
return norms, None
48+
per_tensor_result = torch.stack(per_tensor_norms)
1749
else:
18-
total_norm_sq = sum(flag_gems.sum(flag_gems.pow_func(t.float(), 2)) for t in tensors)
19-
total_norm = flag_gems.sqrt(total_norm_sq)
20-
return total_norm, None
50+
per_tensor_result = torch.tensor(0.0, device=device)
2151

52+
return total_norm, per_tensor_result
2253

23-
def multi_tensor_scale_fl(chunk_size, noop_flag, tensor_lists, scale):
54+
def multi_tensor_scale_fl(
55+
_chunk_size: int,
56+
noop_flag: torch.Tensor,
57+
tensor_lists: List[List[torch.Tensor]],
58+
scale: float,
59+
) -> None:
60+
if noop_flag.item() != 0:
61+
return
2462

2563
for src, dst in zip(tensor_lists[0], tensor_lists[1]):
26-
flag_gems.copy_(dst, src * scale)
64+
# Check for inf/nan (matches CUDA behavior for AMP gradient scaling)
65+
if not torch.isfinite(src).all():
66+
noop_flag.fill_(1)
67+
flag_gems.copy_(dst, flag_gems.mul(src, scale))

0 commit comments

Comments
 (0)