@@ -14,15 +14,20 @@ def multi_tensor_adam_fl(
1414 lr : float ,
1515 beta1 : float ,
1616 beta2 : float ,
17- eps : float ,
17+ epsilon : float ,
1818 step : int ,
1919 mode : int ,
2020 bias_correction : int ,
2121 weight_decay : float ,
2222 inv_scale : Optional [float ] = 1.0 ,
2323 out_dtype : Optional [torch .dtype ] = None ,
2424) -> None :
25+ """
26+ Adam optimizer implementation matching CUDA exactly.
2527
28+ mode == 0: L2 regularization (add weight_decay * param to gradient before moment update)
29+ mode == 1: AdamW (add weight_decay * param to update after moment computation)
30+ """
2631 num_lists = len (tensor_lists )
2732 assert num_lists in [4 , 5 ], f"Expected 4 or 5 tensor lists, got { num_lists } "
2833
@@ -50,66 +55,67 @@ def multi_tensor_adam_fl(
5055 if not g .is_contiguous ():
5156 g = g .contiguous ()
5257
58+ # Convert to float for computation (matches CUDA's MATH_T = float)
59+ g = g .float ()
60+ p_float = p .float ()
61+
5362 if inv_scale is not None and inv_scale != 1.0 :
5463 g = flag_gems .mul (g , inv_scale )
5564
56- m = flag_gems .add_ (flag_gems .mul_ (m , beta1 ), g , alpha = 1 - beta1 )
57- v = flag_gems .add_ (flag_gems .mul_ (v , beta2 ), flag_gems .mul_ (flag_gems .mul_ (g , g ), 1 - beta2 ))
65+ if mode == 0 : # L2 regularization
66+ # Add weight decay to gradient before moment update
67+ g = flag_gems .add (g , p_float , alpha = weight_decay )
68+
69+ # Update moments with modified gradient
70+ flag_gems .add_ (flag_gems .mul_ (m , beta1 ), g , alpha = 1 - beta1 )
71+ flag_gems .add_ (flag_gems .mul_ (v , beta2 ), flag_gems .mul (g , g ), alpha = 1 - beta2 )
72+
73+ # Bias correction
74+ m_corr = flag_gems .true_divide (m , bias_correction1 )
75+ v_corr = flag_gems .true_divide (v , bias_correction2 )
5876
59- m_corr = m .clone ()
60- v_corr = v .clone ()
61- if bias_correction == 1 :
62- m_corr = flag_gems .true_divide (m_corr , bias_correction1 )
63- v_corr = flag_gems .true_divide (v_corr , bias_correction2 )
77+ # Compute update
78+ denom = flag_gems .add (flag_gems .sqrt (v_corr ), epsilon )
79+ update = flag_gems .true_divide (m_corr , denom )
6480
65- update = flag_gems .true_divide (m_corr , flag_gems .add (flag_gems .sqrt (v_corr ), eps ))
81+ # Update parameter
82+ p .add_ (update , alpha = - lr )
83+ else : # mode == 1, AdamW (decoupled weight decay)
84+ # Update moments with original gradient
85+ flag_gems .add_ (flag_gems .mul_ (m , beta1 ), g , alpha = 1 - beta1 )
86+ flag_gems .add_ (flag_gems .mul_ (v , beta2 ), flag_gems .mul (g , g ), alpha = 1 - beta2 )
6687
67- if is_adamw :
68- p = flag_gems .mul_ (p , 1 - lr * weight_decay )
69- else :
70- update = flag_gems .add_ (update , p , alpha = weight_decay )
88+ # Bias correction
89+ m_corr = flag_gems .true_divide (m , bias_correction1 )
90+ v_corr = flag_gems .true_divide (v , bias_correction2 )
7191
72- p = flag_gems .add_ (p , update , alpha = - lr )
92+ # Compute update with weight decay added (matches CUDA exactly)
93+ denom = flag_gems .add (flag_gems .sqrt (v_corr ), epsilon )
94+ update = flag_gems .add (flag_gems .true_divide (m_corr , denom ), p_float , alpha = weight_decay )
95+
96+ # Update parameter
97+ p .add_ (update , alpha = - lr )
7398
7499 if p_master is not None :
75100 flag_gems .copy_ (p_master , p )
76101 out_dtype = p_master .dtype if out_dtype is None else out_dtype
77102 p .data = p .data .to (out_dtype )
78103
79-
80104def multi_tensor_adam_param_remainder_fl (
81105 chunk_size : int ,
82106 noop_flag : torch .Tensor ,
83107 tensor_lists : List [List [torch .Tensor ]],
84108 lr : float ,
85109 beta1 : float ,
86110 beta2 : float ,
87- eps : float ,
111+ epsilon : float ,
88112 step : int ,
89113 mode : int ,
90114 bias_correction : int ,
91115 weight_decay : float ,
92- inv_scale : Optional [float ] = 1.0 ,
93116) -> None :
94117 """
95118 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
113119 """
114120 if noop_flag .item () != 0 :
115121 return
@@ -133,58 +139,78 @@ def multi_tensor_adam_param_remainder_fl(
133139
134140 for i in range (num_tensors ):
135141 g = tensor_lists [0 ][i ]
136- p = tensor_lists [1 ][i ] # BF16 parameter
142+ p = tensor_lists [1 ][i ] # int16 parameter (high 16 bits of FP32)
137143 m = tensor_lists [2 ][i ] # FP32 first moment
138144 v = tensor_lists [3 ][i ] # FP32 second moment
139- p_remainder = tensor_lists [4 ][i ] # int16 remainder
145+ p_remainder = tensor_lists [4 ][i ] # int16 remainder (low 16 bits of FP32)
140146
141147 if not g .is_contiguous ():
142148 g = g .contiguous ()
143149
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 )
150+ # Convert gradient to float
151+ g_float = g .float ()
152+
153+ # Reconstruct FP32 master weight from int16 param + int16 remainder using bit manipulation
154+ # This matches the CUDA implementation exactly:
155+ # 1. If p_remainder < 0, decrement p (undo rounding)
156+ # 2. Combine high 16 bits (p) and low 16 bits (p_remainder) into FP32
157+ # Note: Use PyTorch native ops for bit manipulation (int16/int32 operations)
158+
159+ local_p = p .view (torch .int16 ).clone ()
160+ local_p_rem = p_remainder .clone ()
147161
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 ))
162+ # Undo rounding: if remainder < 0, decrement p
163+ local_p = torch .where (local_p_rem < 0 , local_p - 1 , local_p )
152164
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 ))
165+ # Combine into FP32 using bit shift operations
166+ # local_p is high 16 bits, local_p_rem is low 16 bits
167+ high_bits = local_p .to (torch .int32 ) << 16
168+ low_bits = local_p_rem .to (torch .int32 ) & 0xFFFF # Mask off sign extension
169+ param_int32 = high_bits | low_bits
170+ param_master = param_int32 .view (torch .float32 )
157171
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 ))
172+ # L2 mode: add weight decay to gradient before updating moments
173+ if not is_adamw and weight_decay != 0 :
174+ g_float = flag_gems .add (g_float , param_master , alpha = weight_decay )
175+
176+ # Update first moment: m = beta1 * m + (1 - beta1) * g
177+ flag_gems .add_ (flag_gems .mul_ (m , beta1 ), g_float , alpha = 1 - beta1 )
178+
179+ # Update second moment: v = beta2 * v + (1 - beta2) * g^2
180+ flag_gems .add_ (flag_gems .mul_ (v , beta2 ), flag_gems .mul (g_float , g_float ), alpha = 1 - beta2 )
161181
162182 # 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 )
183+ m_corr = flag_gems . true_divide ( m , bias_correction1 )
184+ v_corr = flag_gems . true_divide ( v , bias_correction2 )
185+
186+ # Compute denominator: sqrt(v_corr) + epsilon
187+ denom = flag_gems .add ( flag_gems . sqrt ( v_corr ), epsilon )
168188
169189 # Compute update
170- update = flag_gems .true_divide (m_corr , flag_gems .add (flag_gems .sqrt (v_corr ), eps ))
190+ update = flag_gems .true_divide (m_corr , denom )
191+
192+ # AdamW mode: add decoupled weight decay to update
193+ if is_adamw and weight_decay != 0 :
194+ update = flag_gems .add (update , param_master , alpha = weight_decay )
171195
172- # Apply weight decay (if AdamW mode)
173- if is_adamw :
174- param_master = flag_gems .mul_ (param_master , 1 - lr * weight_decay )
196+ # Update master weight: p = p - lr * update
197+ param_master = flag_gems .sub (param_master , flag_gems .mul (update , lr ))
175198
176- # Update master weight
177- param_master = flag_gems .add_ (param_master , update , alpha = - lr )
199+ # Split FP32 back into int16 param + int16 remainder using bit manipulation
200+ # This matches the CUDA implementation exactly:
201+ # 1. Extract high 16 bits as p
202+ # 2. Extract low 16 bits as p_remainder
203+ # 3. If p_remainder < 0, increment p (round up)
204+ # Note: Use PyTorch native ops for bit manipulation (int32 operations)
178205
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 )
206+ param_int32 = param_master .view (torch .int32 )
207+ # Extract low 16 bits (remainder) and high 16 bits (param)
208+ new_p_rem = (param_int32 & 0xFFFF ).to (torch .int16 )
209+ new_p = ((param_int32 >> 16 ) & 0xFFFF ).to (torch .int16 )
182210
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 )
211+ # Round up: if remainder < 0, increment p
212+ new_p = torch .where (new_p_rem < 0 , new_p + 1 , new_p )
187213
188214 # Write back
189- flag_gems . copy_ (p , param_bf16 )
190- flag_gems .copy_ (p_remainder , remainder_int16 )
215+ p . view ( torch . int16 ). copy_ (new_p )
216+ p_remainder .copy_ (new_p_rem )
0 commit comments