Skip to content

Commit 4fbf24f

Browse files
authored
[BugFix] fix w4a8 weight loading of DSACP in sfa_v1.py (vllm-project#10523)
### What this PR does / why we need it? This PR refactors the parameter switching mechanism for `o_proj` in the Ascend SFA attention backend. Instead of hardcoding specific parameters like `aclnn_input_scale`, it introduces configurable parameter groups (`O_PROJ_ACLNN_INPUT_PARAMS` and `O_PROJ_INPUT_SHARDED_QUANT_PARAMS`) and a helper method `_switch_o_proj_params` to dynamically switch between TP-mode and Full-mode. - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@967c5c3 --------- Signed-off-by: QiuChunshuo <qiuchunshuo@huawei.com>
1 parent f302cf7 commit 4fbf24f

1 file changed

Lines changed: 96 additions & 129 deletions

File tree

vllm_ascend/attention/sfa_v1.py

Lines changed: 96 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,13 @@
7171
# token count limits within bmm_transpose operator
7272
BMM_TRANS_MAX_SUPPORTED_TOKENS = 1024
7373

74+
O_PROJ_ACLNN_INPUT_PARAMS = (
75+
"aclnn_input_scale",
76+
"aclnn_input_scale_reciprocal",
77+
"aclnn_input_offset",
78+
)
79+
O_PROJ_INPUT_SHARDED_QUANT_PARAMS = ("weight_scale_second", "weight_scale")
80+
7481

7582
class AscendSFABackend(AttentionBackend):
7683
accept_output_buffer: bool = True
@@ -432,8 +439,7 @@ class AscendSFAImpl(MLAAttentionImpl):
432439
"""
433440

434441
# Supports forward using the all-gather o_proj weight for decode requests when Sharded CP is enabled.
435-
o_proj_full_pool: torch.Tensor | None = None
436-
o_proj_full_weight_scale_pool: torch.Tensor | None = None
442+
o_proj_full_pools: dict[tuple[str, int | None, torch.dtype, int, tuple[int, ...]], torch.Tensor] = {}
437443

438444
# q_hadamard and k_hadamard tensor shared when dsa c8 enabled
439445
q_hadamard: torch.Tensor | None = None
@@ -537,7 +543,6 @@ def __init__(
537543
# use original TP o_proj weight in PD mix stage, and full gather
538544
# for o_proj weight for prefill stage.
539545
self.enable_dsa_cp_with_o_proj_tp = enable_dsa_cp_with_o_proj_tp()
540-
self._o_proj_dynamic_quant = False
541546

542547
if self.enable_dsa_cp:
543548
self.local_num_heads = self.num_heads * self.tp_size
@@ -612,7 +617,7 @@ def process_weights_after_loading(self, act_dtype: torch.dtype):
612617
if is_hidden_layer(layer):
613618
post_process_after_loading_for_shard_weight_series(layer)
614619
else:
615-
self._maybe_init_o_proj_tp_full_params()
620+
self._init_o_proj_tp_full_params()
616621

617622
if self.enable_mlapo:
618623
quant_method = getattr(
@@ -813,46 +818,6 @@ def rope_single(
813818
x = torch_npu.npu_interleave_rope(x, cos, sin)
814819
return x.view(B, N, D)
815820

816-
def _check_o_proj_dynamic_quant(self) -> bool:
817-
return hasattr(self.o_proj, "weight_scale")
818-
819-
def _maybe_init_o_proj_tp_full_params(self):
820-
if self._check_o_proj_dynamic_quant():
821-
self._o_proj_dynamic_quant = True
822-
self._init_dynamic_quant_o_proj_tp_full_params()
823-
else:
824-
self._init_o_proj_tp_full_params()
825-
826-
def _init_dynamic_quant_o_proj_tp_full_params(self):
827-
"""
828-
Initialize TP-mode and Full-mode parameters for o_proj weight,
829-
preparing for weight switching in PD mix stage.
830-
831-
For PD mix stage:
832-
- Use original TP o_proj weight for decode phase
833-
- Need full-gather o_proj weight from all TP ranks for prefill phase
834-
"""
835-
if AscendSFAImpl.o_proj_full_pool is None:
836-
sample = self.o_proj.weight
837-
AscendSFAImpl.o_proj_full_pool = torch.empty(
838-
(sample.shape[0] * self.tp_size, sample.shape[1]), dtype=sample.dtype, device=sample.device
839-
)
840-
if AscendSFAImpl.o_proj_full_weight_scale_pool is None:
841-
sample = self.o_proj.weight_scale
842-
AscendSFAImpl.o_proj_full_weight_scale_pool = torch.empty(
843-
(sample.shape[0] * self.tp_size, sample.shape[1], sample.shape[2]),
844-
dtype=sample.dtype,
845-
device=sample.device,
846-
)
847-
848-
# Save TP-mode parameters (original sharded weights)
849-
self.o_proj_tp_weight = self.o_proj.weight.clone().detach()
850-
self.o_proj_tp_weight_scale = self.o_proj.weight_scale.clone().detach()
851-
852-
# Initially switch to TP mode for graph capture
853-
self.o_proj.weight.set_(self.o_proj_tp_weight)
854-
self.o_proj.weight_scale.set_(self.o_proj_tp_weight_scale)
855-
856821
def _init_o_proj_tp_full_params(self):
857822
"""
858823
Initialize TP-mode and Full-mode parameters for o_proj weight,
@@ -862,79 +827,84 @@ def _init_o_proj_tp_full_params(self):
862827
- Use original TP o_proj weight for decode phase
863828
- Need full-gather o_proj weight from all TP ranks for prefill phase
864829
"""
865-
if AscendSFAImpl.o_proj_full_pool is None:
866-
sample = self.o_proj.weight
867-
AscendSFAImpl.o_proj_full_pool = torch.empty(
868-
(sample.shape[0] * self.tp_size, sample.shape[1]), dtype=sample.dtype, device=sample.device
830+
sample = self.o_proj.weight
831+
self.o_proj_full_weight_gather_dim = 1 if self._is_o_proj_unquantized() else 0
832+
if self.o_proj_full_weight_gather_dim == 0:
833+
full_shape = (sample.shape[0] * self.tp_size, sample.shape[1])
834+
gather_shape = full_shape
835+
else:
836+
full_shape = (sample.shape[0], sample.shape[1] * self.tp_size)
837+
gather_shape = (sample.shape[1] * self.tp_size, sample.shape[0])
838+
# Main and MTP layers can use different quantized o_proj weight layouts,
839+
# so key the shared full-gather pool by gather dimension, dtype, and shape.
840+
pool_key = (
841+
sample.device.type,
842+
sample.device.index,
843+
sample.dtype,
844+
self.o_proj_full_weight_gather_dim,
845+
full_shape,
846+
)
847+
if pool_key not in AscendSFAImpl.o_proj_full_pools:
848+
AscendSFAImpl.o_proj_full_pools[pool_key] = torch.empty(
849+
gather_shape, dtype=sample.dtype, device=sample.device
869850
)
851+
self.o_proj_full_gather_pool = AscendSFAImpl.o_proj_full_pools[pool_key]
852+
if self.o_proj_full_weight_gather_dim == 0:
853+
self.o_proj_full_pool = self.o_proj_full_gather_pool
854+
else:
855+
self.o_proj_full_pool = self.o_proj_full_gather_pool.transpose(0, 1)
870856

871857
# Save TP-mode parameters (original sharded weights)
872858
self.o_proj_tp_weight = self.o_proj.weight.clone().detach()
873-
self.o_proj_tp_aclnn_input_scale = self.o_proj.aclnn_input_scale.clone().detach()
874-
self.o_proj_tp_aclnn_input_scale_reciprocal = self.o_proj.aclnn_input_scale_reciprocal.clone().detach()
875-
self.o_proj_tp_aclnn_input_offset = self.o_proj.aclnn_input_offset.clone().detach()
859+
if self.o_proj_full_weight_gather_dim == 0:
860+
self.o_proj_tp_weight_gather_input = self.o_proj_tp_weight
861+
else:
862+
self.o_proj_tp_weight_gather_input = self.o_proj_tp_weight.transpose(0, 1).contiguous()
863+
self.o_proj_tp_aclnn_input_params = {}
864+
self.o_proj_full_aclnn_input_params = {}
865+
for param_name in O_PROJ_ACLNN_INPUT_PARAMS:
866+
param = getattr(self.o_proj, param_name, None)
867+
if param is None:
868+
continue
869+
self.o_proj_tp_aclnn_input_params[param_name] = param.clone().detach()
870+
self.o_proj_full_aclnn_input_params[param_name] = param.repeat(self.tp_size)
871+
872+
self.o_proj_tp_input_sharded_quant_params = {}
873+
self.o_proj_full_input_sharded_quant_params = {}
874+
for param_name in O_PROJ_INPUT_SHARDED_QUANT_PARAMS:
875+
param = getattr(self.o_proj, param_name, None)
876+
if param is None or getattr(param, "input_dim", None) != 1:
877+
continue
878+
self.o_proj_tp_input_sharded_quant_params[param_name] = param.clone().detach()
879+
self.o_proj_full_input_sharded_quant_params[param_name] = torch.empty(
880+
(param.shape[0] * self.tp_size, *param.shape[1:]), dtype=param.dtype, device=param.device
881+
)
876882

877883
# Initially switch to TP mode for graph capture
878884
self.o_proj.weight.set_(self.o_proj_tp_weight)
879-
self.o_proj.aclnn_input_scale.set_(self.o_proj_tp_aclnn_input_scale)
880-
self.o_proj.aclnn_input_scale_reciprocal.set_(self.o_proj_tp_aclnn_input_scale_reciprocal)
881-
self.o_proj.aclnn_input_offset.set_(self.o_proj_tp_aclnn_input_offset)
882-
883-
# Precompute Full-mode quantization parameters by repeating TP parameters across all TP ranks
884-
self.o_proj_full_aclnn_input_scale = self.o_proj.aclnn_input_scale.repeat(self.tp_size)
885-
self.o_proj_full_aclnn_input_scale_reciprocal = self.o_proj.aclnn_input_scale_reciprocal.repeat(self.tp_size)
886-
self.o_proj_full_aclnn_input_offset = self.o_proj.aclnn_input_offset.repeat(self.tp_size)
887-
888-
def _handle_dynamic_quant_o_proj_weight_switch_and_forward(
889-
self,
890-
attn_output: torch.Tensor,
891-
output: torch.Tensor,
892-
o_proj_full_handle: torch.distributed.Work | None,
893-
o_proj_full_weight_scale_handle: torch.distributed.Work | None,
894-
should_shard_weight: bool,
895-
) -> tuple[torch.Tensor, bool]:
896-
"""
897-
Handle o_proj weight switching between TP-mode and Full-mode, and execute forward computation.
898-
"""
899-
# Gather o_proj weight from all TP ranks for Full-mode computation
900-
if should_shard_weight:
901-
# Wait for the completion of o_proj weight all-gather operation
902-
if o_proj_full_handle is not None:
903-
o_proj_full_handle.wait()
904-
if o_proj_full_weight_scale_handle is not None:
905-
o_proj_full_weight_scale_handle.wait()
885+
self._switch_o_proj_params(self.o_proj_tp_aclnn_input_params)
886+
self._switch_o_proj_params(self.o_proj_tp_input_sharded_quant_params)
906887

907-
# Switch o_proj to Full-mode (gathered weight from all TP ranks)
908-
self.o_proj.weight.set_(AscendSFAImpl.o_proj_full_pool)
909-
self.o_proj.weight_scale.set_(AscendSFAImpl.o_proj_full_weight_scale_pool)
888+
def _switch_o_proj_params(self, params: dict[str, torch.Tensor]):
889+
for param_name, param in params.items():
890+
getattr(self.o_proj, param_name).set_(param)
910891

911-
# Apply quantization method and execute forward computation
912-
output[...] = self.o_proj.quant_method.quant_method.apply(self.o_proj, attn_output)
892+
def _get_o_proj_linear_method(self):
893+
quant_method = self.o_proj.quant_method
894+
return getattr(quant_method, "quant_method", quant_method)
913895

914-
# Switch o_proj back to TP-mode for subsequent decode operations
915-
self.o_proj.weight.set_(self.o_proj_tp_weight)
916-
self.o_proj.weight_scale.set_(self.o_proj_tp_weight_scale)
896+
def _is_o_proj_unquantized(self) -> bool:
897+
return isinstance(self._get_o_proj_linear_method(), UnquantizedLinearMethod)
917898

918-
return output, False
919-
else:
920-
# For decode scenario: perform all-to-all communication on o_proj input activations
921-
# Reshape for all-to-all: [batch * seq, tp_size, head_dim] -> [tp_size, batch * seq, head_dim]
922-
send = (
923-
attn_output.view(-1, self.tp_size, self.num_heads * self.v_head_dim)
924-
.permute(1, 0, 2)
925-
.reshape(-1, self.num_heads * self.v_head_dim)
926-
)
927-
928-
attn_output = torch.empty_like(send)
929-
torch.distributed.all_to_all_single(attn_output, send, group=get_tp_group().device_group)
930-
931-
return attn_output, True
899+
def _apply_o_proj_full_weight(self, attn_output: torch.Tensor) -> torch.Tensor:
900+
return self._get_o_proj_linear_method().apply(self.o_proj, attn_output)
932901

933902
def _handle_o_proj_weight_switch_and_forward(
934903
self,
935904
attn_output: torch.Tensor,
936905
output: torch.Tensor,
937906
o_proj_full_handle: torch.distributed.Work | None,
907+
o_proj_full_param_handles: list[torch.distributed.Work | None] | None,
938908
should_shard_weight: bool,
939909
) -> tuple[torch.Tensor, bool]:
940910
"""
@@ -945,21 +915,22 @@ def _handle_o_proj_weight_switch_and_forward(
945915
# Wait for the completion of o_proj weight all-gather operation
946916
if o_proj_full_handle is not None:
947917
o_proj_full_handle.wait()
918+
for handle in o_proj_full_param_handles or []:
919+
if handle is not None:
920+
handle.wait()
948921

949922
# Switch o_proj to Full-mode (gathered weight from all TP ranks)
950-
self.o_proj.weight.set_(AscendSFAImpl.o_proj_full_pool)
951-
self.o_proj.aclnn_input_scale.set_(self.o_proj_full_aclnn_input_scale)
952-
self.o_proj.aclnn_input_scale_reciprocal.set_(self.o_proj_full_aclnn_input_scale_reciprocal)
953-
self.o_proj.aclnn_input_offset.set_(self.o_proj_full_aclnn_input_offset)
923+
self.o_proj.weight.set_(self.o_proj_full_pool)
924+
self._switch_o_proj_params(self.o_proj_full_aclnn_input_params)
925+
self._switch_o_proj_params(self.o_proj_full_input_sharded_quant_params)
954926

955927
# Apply quantization method and execute forward computation
956-
output[...] = self.o_proj.quant_method.quant_method.apply(self.o_proj, attn_output)
928+
output[...] = self._apply_o_proj_full_weight(attn_output)
957929

958930
# Switch o_proj back to TP-mode for subsequent decode operations
959931
self.o_proj.weight.set_(self.o_proj_tp_weight)
960-
self.o_proj.aclnn_input_scale.set_(self.o_proj_tp_aclnn_input_scale)
961-
self.o_proj.aclnn_input_scale_reciprocal.set_(self.o_proj_tp_aclnn_input_scale_reciprocal)
962-
self.o_proj.aclnn_input_offset.set_(self.o_proj_tp_aclnn_input_offset)
932+
self._switch_o_proj_params(self.o_proj_tp_aclnn_input_params)
933+
self._switch_o_proj_params(self.o_proj_tp_input_sharded_quant_params)
963934

964935
return output, False
965936
else:
@@ -1284,7 +1255,7 @@ def forward(
12841255

12851256
# all-gather o_proj weight for prefill stage of PD mix node
12861257
o_proj_full_handle = None
1287-
o_proj_full_weight_scale_handle = None
1258+
o_proj_full_param_handles = None
12881259
# if is PD mix stage, using original TP o_proj weight, and also need to full gather for o_proj
12891260
# weight for prefill stage.
12901261
full_gather_o_proj_enabled = self.enable_dsa_cp_with_o_proj_tp and attn_metadata.attn_state not in {
@@ -1423,14 +1394,18 @@ def forward(
14231394
reach_layer_for_shard_weight_series(layer)
14241395
elif full_gather_o_proj_enabled:
14251396
_, o_proj_full_handle = all_gather_async(
1426-
self.o_proj_tp_weight, get_tp_group(), output=AscendSFAImpl.o_proj_full_pool
1397+
self.o_proj_tp_weight_gather_input,
1398+
get_tp_group(),
1399+
output=self.o_proj_full_gather_pool,
14271400
)
1428-
if self._o_proj_dynamic_quant:
1429-
_, o_proj_full_weight_scale_handle = all_gather_async(
1430-
self.o_proj_tp_weight_scale,
1401+
o_proj_full_param_handles = []
1402+
for param_name, param in self.o_proj_tp_input_sharded_quant_params.items():
1403+
_, param_handle = all_gather_async(
1404+
param,
14311405
get_tp_group(),
1432-
output=AscendSFAImpl.o_proj_full_weight_scale_pool,
1406+
output=self.o_proj_full_input_sharded_quant_params[param_name],
14331407
)
1408+
o_proj_full_param_handles.append(param_handle)
14341409

14351410
if kv_cache is not None:
14361411
assert fused_kv_no_split is not None
@@ -1544,21 +1519,13 @@ def forward(
15441519
# When using SFA-CP with pd mixed, o_proj has two cases:
15451520
# 1. prefill: o_proj is a TP weight, we need to all-gather o_proj weight to switch TP=1.
15461521
# 2. decode: all-to-all the hidden_state before the o_proj forward.
1547-
if self._o_proj_dynamic_quant:
1548-
result, require_o_proj_forward = self._handle_dynamic_quant_o_proj_weight_switch_and_forward(
1549-
attn_output=attn_output,
1550-
output=output,
1551-
o_proj_full_handle=o_proj_full_handle,
1552-
o_proj_full_weight_scale_handle=o_proj_full_weight_scale_handle,
1553-
should_shard_weight=full_gather_o_proj_enabled,
1554-
)
1555-
else:
1556-
result, require_o_proj_forward = self._handle_o_proj_weight_switch_and_forward(
1557-
attn_output=attn_output,
1558-
output=output,
1559-
o_proj_full_handle=o_proj_full_handle,
1560-
should_shard_weight=full_gather_o_proj_enabled,
1561-
)
1522+
result, require_o_proj_forward = self._handle_o_proj_weight_switch_and_forward(
1523+
attn_output=attn_output,
1524+
output=output,
1525+
o_proj_full_handle=o_proj_full_handle,
1526+
o_proj_full_param_handles=o_proj_full_param_handles,
1527+
should_shard_weight=full_gather_o_proj_enabled,
1528+
)
15621529
if not require_o_proj_forward:
15631530
return result
15641531
attn_output = result

0 commit comments

Comments
 (0)