diff --git a/lightllm/common/basemodel/layer_infer/template/transformer_layer_infer_template.py b/lightllm/common/basemodel/layer_infer/template/transformer_layer_infer_template.py index f0cc129c09..ed9181c463 100755 --- a/lightllm/common/basemodel/layer_infer/template/transformer_layer_infer_template.py +++ b/lightllm/common/basemodel/layer_infer/template/transformer_layer_infer_template.py @@ -3,8 +3,9 @@ import torch.distributed as dist from ..transformer_layer_infer import TransformerLayerInfer from ...infer_struct import InferStateInfo -from lightllm.distributed import all_reduce +from lightllm.distributed import all_reduce, all_reduce_fused_add_rmsnorm from typing import Tuple +from lightllm.utils.envs_utils import get_env_start_args from lightllm.utils.tensor_utils import tensor_to_no_ref_tensor @@ -21,8 +22,48 @@ def __init__(self, layer_num, network_config): self.tp_o_head_num_ = -1 self.head_dim_ = -1 self.embed_dim_ = -1 + # Subclasses that use a plain RMSNorm after the post-attention residual add + # (gamma tensor at ``layer_weight.ffn_norm_weight_.weight``) can set this True + # to fuse all_reduce + residual add + RMSNorm via FlashInfer. See + # _should_fuse_ar_add_norm / _reduce_add_ffn_norm. + self._enable_fused_ar_add_norm = False return + def _should_fuse_ar_add_norm(self, infer_state: InferStateInfo) -> bool: + # Only the plain-TP all-reduce path is fusible: skip under tpsp mix mode + # (reduce-scatter) and when the FlashInfer all-reduce backend is absent. + if not self._enable_fused_ar_add_norm or self.tp_world_size_ <= 1: + return False + args = get_env_start_args() + if args.enable_tpsp_mix_mode or args.disable_fused_allreduce_norm: + return False + return getattr(infer_state.dist_group, "flashinfer_reduce", None) is not None + + def _reduce_add_ffn_norm(self, o, input_embdings, infer_state: InferStateInfo, layer_weight) -> torch.Tensor: + # o is the pre-reduce partial o_proj output (reduce was deferred in _get_o). + # Fuse all_reduce + (input_embdings += o) + ffn_norm, else do them separately. + # ponytail: only the post-attention reduce+add+norm is fused. The post-ffn + # reduce+add is followed by the *next* layer's att_norm, which lives in a + # different token_forward call, so it is left unfused. Fusing it too would + # need threading a separate residual across the layer loop (vLLM-style) -- + # a much larger, riskier refactor for the second half of the win. + o = o.view(-1, self.embed_dim_) + flashinfer_reduce = getattr(infer_state.dist_group, "flashinfer_reduce", None) + if flashinfer_reduce is None or not flashinfer_reduce.should_use(o): + all_reduce(o, group=infer_state.dist_group) + input_embdings.add_(o) + return self._ffn_norm(input_embdings, infer_state, layer_weight) + + norm_out = self.alloc_tensor(o.shape, o.dtype) + fused = all_reduce_fused_add_rmsnorm( + o, input_embdings, layer_weight.ffn_norm_weight_.weight, self.eps_, norm_out, group=infer_state.dist_group + ) + if fused: + return norm_out + all_reduce(o, group=infer_state.dist_group) + input_embdings.add_(o) + return self._ffn_norm(input_embdings, infer_state, layer_weight) + def _att_norm(self, input, infer_state: InferStateInfo, layer_weight) -> torch.Tensor: raise Exception("need to impl") @@ -47,52 +88,70 @@ def _context_attention_kernel(self, q, kv, infer_state: InferStateInfo, layer_we def _token_attention_kernel(self, q, infer_state: InferStateInfo, layer_weight, out=None) -> torch.Tensor: raise Exception("need to impl") - def _get_o(self, input, infer_state: InferStateInfo, layer_weight) -> torch.Tensor: + def _get_o(self, input, infer_state: InferStateInfo, layer_weight, defer_reduction=False) -> torch.Tensor: raise Exception("need to impl") def _ffn(self, input, infer_state: InferStateInfo, layer_weight) -> torch.Tensor: raise Exception("need to impl") - def context_attention_forward(self, input_embdings, infer_state: InferStateInfo, layer_weight): + def context_attention_forward( + self, input_embdings, infer_state: InferStateInfo, layer_weight, defer_reduction=False + ): q, cache_kv = self._get_qkv(input_embdings, infer_state, layer_weight) self._post_cache_kv(cache_kv, infer_state, layer_weight) o = self._context_attention_wrapper_run( q=q, cache_kv=cache_kv, infer_state=infer_state, layer_weight=layer_weight ) q = None - o = self._get_o(o, infer_state, layer_weight) + if defer_reduction: + o = self._get_o(o, infer_state, layer_weight, defer_reduction=True) + else: + o = self._get_o(o, infer_state, layer_weight) return o def context_forward(self, input_embdings, infer_state: InferStateInfo, layer_weight): input1 = self._att_norm(input_embdings, infer_state, layer_weight) - o = self.context_attention_forward(input1, infer_state, layer_weight) - input_embdings.add_(o.view(-1, self.embed_dim_)) + use_fused_reduce = self._should_fuse_ar_add_norm(infer_state) + if use_fused_reduce: + o = self.context_attention_forward(input1, infer_state, layer_weight, defer_reduction=True) + input1 = self._reduce_add_ffn_norm(o, input_embdings, infer_state, layer_weight) + else: + o = self.context_attention_forward(input1, infer_state, layer_weight) + input_embdings.add_(o.view(-1, self.embed_dim_)) + input1 = self._ffn_norm(input_embdings, infer_state, layer_weight) o = None - input1 = self._ffn_norm(input_embdings, infer_state, layer_weight) ffn_out = self._ffn(input1, infer_state, layer_weight) input1 = None input_embdings.add_(ffn_out.view(-1, self.embed_dim_)) return input_embdings - def token_attention_forward(self, input_embdings, infer_state: InferStateInfo, layer_weight): + def token_attention_forward(self, input_embdings, infer_state: InferStateInfo, layer_weight, defer_reduction=False): q, cache_kv = self._get_qkv(input_embdings, infer_state, layer_weight) self._post_cache_kv(cache_kv, infer_state, layer_weight) o = self._token_attention_kernel(q, infer_state, layer_weight) q = None - o = self._get_o(o, infer_state, layer_weight) + if defer_reduction: + o = self._get_o(o, infer_state, layer_weight, defer_reduction=True) + else: + o = self._get_o(o, infer_state, layer_weight) return o def token_forward(self, input_embdings, infer_state: InferStateInfo, layer_weight): input1 = self._att_norm(input_embdings, infer_state, layer_weight) - o = self.token_attention_forward(input1, infer_state, layer_weight) - input_embdings.add_(o.view(-1, self.embed_dim_)) + use_fused_reduce = self._should_fuse_ar_add_norm(infer_state) + if use_fused_reduce: + o = self.token_attention_forward(input1, infer_state, layer_weight, defer_reduction=True) + input1 = self._reduce_add_ffn_norm(o, input_embdings, infer_state, layer_weight) + else: + o = self.token_attention_forward(input1, infer_state, layer_weight) + input_embdings.add_(o.view(-1, self.embed_dim_)) + input1 = self._ffn_norm(input_embdings, infer_state, layer_weight) o = None - input1 = self._ffn_norm(input_embdings, infer_state, layer_weight) ffn_out = self._ffn(input1, infer_state, layer_weight) input_embdings.add_(ffn_out.view(-1, self.embed_dim_)) diff --git a/lightllm/distributed/communication_op.py b/lightllm/distributed/communication_op.py index f15badde25..d748e6c1f5 100644 --- a/lightllm/distributed/communication_op.py +++ b/lightllm/distributed/communication_op.py @@ -100,6 +100,22 @@ def all_reduce(self, input_: torch.Tensor) -> None: return return dist.all_reduce(input_, group=self.device_group) + def all_reduce_fused_add_rmsnorm( + self, + input_: torch.Tensor, + residual: torch.Tensor, + weight: torch.Tensor, + eps: float, + norm_out: torch.Tensor, + ) -> bool: + # Fuse all_reduce + residual add + RMSNorm via FlashInfer when the message is + # small enough for its oneshot path. Returns True if fused (residual & norm_out + # filled), False if the caller must fall back to plain all_reduce + add + norm. + if self.flashinfer_reduce is not None and self.flashinfer_reduce.should_use(input_): + self.flashinfer_reduce.all_reduce_fused_add_rmsnorm(input_, residual, weight, eps, norm_out) + return True + return False + def all_gather_into_tensor(self, output_: torch.Tensor, input_: torch.Tensor, async_op: bool = False) -> None: return dist.all_gather_into_tensor(output_, input_, group=self.device_group, async_op=async_op) @@ -235,6 +251,25 @@ def all_reduce( return dist.all_reduce(input_, op, group, async_op) +def all_reduce_fused_add_rmsnorm( + input_: torch.Tensor, + residual: torch.Tensor, + weight: torch.Tensor, + eps: float, + norm_out: torch.Tensor, + group: Optional[Union[ProcessGroup, CustomProcessGroup]] = None, +) -> bool: + """Try the FlashInfer all_reduce + residual add + RMSNorm fusion. + + Returns True on success (``residual`` and ``norm_out`` are filled). Returns + False when unavailable (no custom group, message too large, dtype/world-size + unsupported); the caller then does plain all_reduce + add + norm itself. + """ + if isinstance(group, CustomProcessGroup): + return group.all_reduce_fused_add_rmsnorm(input_, residual, weight, eps, norm_out) + return False + + def all_gather_into_tensor( output_: torch.Tensor, input_: torch.Tensor, diff --git a/lightllm/distributed/flashinfer_all_reduce.py b/lightllm/distributed/flashinfer_all_reduce.py index 35cb0aaedf..c54f5f629a 100644 --- a/lightllm/distributed/flashinfer_all_reduce.py +++ b/lightllm/distributed/flashinfer_all_reduce.py @@ -134,3 +134,28 @@ def all_reduce(self, inp: torch.Tensor) -> torch.Tensor: pattern=flashinfer_comm.AllReduceFusionPattern.kAllReduce, # launch_with_pdl=True, # TODO: learn pdl and ensure no other side effects. ) + + def all_reduce_fused_add_rmsnorm( + self, + inp: torch.Tensor, + residual: torch.Tensor, + weight: torch.Tensor, + eps: float, + norm_out: torch.Tensor, + ) -> None: + """Fused: residual += all_reduce(inp); norm_out = rmsnorm(residual, weight). + + ``residual`` is updated in place (residual_in aliases residual_out). Same + size / dtype constraints as ``all_reduce`` -- gate with ``should_use``. + """ + flashinfer_comm.allreduce_fusion( + input=inp, + workspace=self._workspace, + pattern=flashinfer_comm.AllReduceFusionPattern.kARResidualRMSNorm, + residual_in=residual, + residual_out=residual, + norm_out=norm_out, + rms_gamma=weight, + rms_eps=eps, + ) + return diff --git a/lightllm/models/deepseek2/layer_infer/transformer_layer_infer.py b/lightllm/models/deepseek2/layer_infer/transformer_layer_infer.py index dae79cc8a6..497fd4f2ec 100644 --- a/lightllm/models/deepseek2/layer_infer/transformer_layer_infer.py +++ b/lightllm/models/deepseek2/layer_infer/transformer_layer_infer.py @@ -200,7 +200,11 @@ def _get_qkv( return q, cache_kv def _get_o( - self, input: torch.Tensor, infer_state: Deepseek2InferStateInfo, layer_weight: Deepseek2TransformerLayerWeight + self, + input: torch.Tensor, + infer_state: Deepseek2InferStateInfo, + layer_weight: Deepseek2TransformerLayerWeight, + defer_reduction=False, ) -> torch.Tensor: if infer_state.need_dp_prefill_balance: input = infer_state._all_to_all_balance_get(data=input) @@ -208,6 +212,8 @@ def _get_o( if input.shape[2] == self.kv_lora_rank: input = layer_weight.v_b_proj_.bmm(input.transpose(0, 1)).transpose(0, 1) o_tensor = layer_weight.o_weight_.mm(input.reshape(-1, self.tp_q_head_num_ * self.v_head_dim)) + if defer_reduction: + return o_tensor o_tensor = self._tpsp_reduce(input=o_tensor, infer_state=infer_state) return o_tensor diff --git a/lightllm/models/llama/layer_infer/transformer_layer_infer.py b/lightllm/models/llama/layer_infer/transformer_layer_infer.py index 69acffaa4d..68e43881d9 100644 --- a/lightllm/models/llama/layer_infer/transformer_layer_infer.py +++ b/lightllm/models/llama/layer_infer/transformer_layer_infer.py @@ -25,6 +25,9 @@ def __init__(self, layer_num, network_config): self.tp_o_head_num_ = self.tp_q_head_num_ self.head_dim_ = network_config["hidden_size"] // network_config["num_attention_heads"] self.embed_dim_ = network_config["hidden_size"] + # Llama uses a plain RMSNorm after the post-attention residual add, so the + # all_reduce + add + ffn_norm sequence can be fused (see the template). + self._enable_fused_ar_add_norm = True self._bind_func() return @@ -97,7 +100,11 @@ def _get_qkv( return q, cache_kv def _get_o( - self, input, infer_state: LlamaInferStateInfo, layer_weight: LlamaTransformerLayerWeight + self, + input, + infer_state: LlamaInferStateInfo, + layer_weight: LlamaTransformerLayerWeight, + defer_reduction=False, ) -> torch.Tensor: if infer_state.need_dp_prefill_balance: input = infer_state._all_to_all_balance_get(data=input) @@ -105,6 +112,10 @@ def _get_o( input = input.view(-1, self.tp_o_head_num_ * self.head_dim_) o_tensor = layer_weight.o_proj.mm(input) + # When fusing, defer the all-reduce to the fused reduce+add+ffn_norm op in + # the template; return the pre-reduce partial here. + if defer_reduction: + return o_tensor o_tensor = self._tpsp_reduce(input=o_tensor, infer_state=infer_state) return o_tensor diff --git a/lightllm/models/qwen3next/layer_infer/transformer_layer_infer.py b/lightllm/models/qwen3next/layer_infer/transformer_layer_infer.py index 92d68c9fd2..49b9f3f738 100644 --- a/lightllm/models/qwen3next/layer_infer/transformer_layer_infer.py +++ b/lightllm/models/qwen3next/layer_infer/transformer_layer_infer.py @@ -167,6 +167,7 @@ def _get_o( input, infer_state: Qwen3NextInferStateInfo, layer_weight: Qwen3NextTransformerLayerWeight, + defer_reduction=False, ) -> torch.Tensor: """Output projection with gating (in-place multiply to save one allocation).""" if infer_state.need_dp_prefill_balance: @@ -175,6 +176,8 @@ def _get_o( sigmoid_mul_(input, infer_state.gate_logics_value) infer_state.gate_logics_value = None o_tensor = layer_weight.o_proj.mm(input) + if defer_reduction: + return o_tensor o_tensor = self._tpsp_reduce(input=o_tensor, infer_state=infer_state) return o_tensor @@ -240,10 +243,13 @@ def context_attention_forward( input_embdings, infer_state: Qwen3NextInferStateInfo, layer_weight: Qwen3NextTransformerLayerWeight, + defer_reduction=False, ): # full attention layer if not self.is_linear_attention_layer: - return super().context_attention_forward(input_embdings, infer_state, layer_weight) + return super().context_attention_forward( + input_embdings, infer_state, layer_weight, defer_reduction=defer_reduction + ) assert isinstance(infer_state.mem_manager, Qwen3NextMemManager) mixed_qkvzba = self._linear_in_proj(input_embdings, layer_weight) @@ -268,7 +274,7 @@ def context_attention_forward( gdn_out = self._linear_post(core_attn_out, z, layer_weight) - if self.tp_world_size_ > 1: + if self.tp_world_size_ > 1 and not defer_reduction: all_reduce(gdn_out, op=dist.ReduceOp.SUM, group=infer_state.dist_group, async_op=False) return gdn_out @@ -277,9 +283,12 @@ def token_attention_forward( input_embdings, infer_state: Qwen3NextInferStateInfo, layer_weight: Qwen3NextTransformerLayerWeight, + defer_reduction=False, ): if not self.is_linear_attention_layer: - return super().token_attention_forward(input_embdings, infer_state, layer_weight) + return super().token_attention_forward( + input_embdings, infer_state, layer_weight, defer_reduction=defer_reduction + ) assert isinstance(infer_state.mem_manager, Qwen3NextMemManager) mixed_qkvzba = self._linear_in_proj(input_embdings, layer_weight) @@ -299,7 +308,7 @@ def token_attention_forward( ) gdn_out = self._linear_post(core_attn_out, z, layer_weight) - if self.tp_world_size_ > 1: + if self.tp_world_size_ > 1 and not defer_reduction: all_reduce(gdn_out, op=dist.ReduceOp.SUM, group=infer_state.dist_group, async_op=False) return gdn_out diff --git a/lightllm/models/stablelm/layer_infer/transformer_layer_infer.py b/lightllm/models/stablelm/layer_infer/transformer_layer_infer.py index f34619b1f8..360dcf474b 100755 --- a/lightllm/models/stablelm/layer_infer/transformer_layer_infer.py +++ b/lightllm/models/stablelm/layer_infer/transformer_layer_infer.py @@ -9,6 +9,7 @@ class StablelmTransformerLayerInfer(LlamaTransformerLayerInfer): def __init__(self, layer_num, network_config): super().__init__(layer_num, network_config) + self._enable_fused_ar_add_norm = False self.partial_rotary_factor = self.network_config_.get("partial_rotary_factor", 1) return @@ -38,13 +39,19 @@ def _get_qkv( return q, cache_kv def _get_o( - self, input, infer_state: LlamaInferStateInfo, layer_weight: StablelmTransformerLayerWeight + self, + input, + infer_state: LlamaInferStateInfo, + layer_weight: StablelmTransformerLayerWeight, + defer_reduction=False, ) -> torch.Tensor: if infer_state.need_dp_prefill_balance: input = infer_state._all_to_all_balance_get(data=input) o_tensor = layer_weight.o_proj.mm( input.view(-1, self.tp_o_head_num_ * self.head_dim_), ) + if defer_reduction: + return o_tensor o_tensor = self._tpsp_reduce(input=o_tensor, infer_state=infer_state) return o_tensor diff --git a/lightllm/models/starcoder2/layer_infer/transformer_layer_infer.py b/lightllm/models/starcoder2/layer_infer/transformer_layer_infer.py index a4347e4a06..51e4e79953 100644 --- a/lightllm/models/starcoder2/layer_infer/transformer_layer_infer.py +++ b/lightllm/models/starcoder2/layer_infer/transformer_layer_infer.py @@ -7,6 +7,7 @@ class Starcoder2TransformerLayerInfer(LlamaTransformerLayerInfer): def __init__(self, layer_num, network_config): super().__init__(layer_num, network_config) + self._enable_fused_ar_add_norm = False def _att_norm( self, input, infer_state: LlamaInferStateInfo, layer_weight: Starcoder2TransformerLayerWeight diff --git a/lightllm/server/api_cli.py b/lightllm/server/api_cli.py index e85d083075..0638568eb6 100644 --- a/lightllm/server/api_cli.py +++ b/lightllm/server/api_cli.py @@ -357,6 +357,13 @@ def add_cli_args(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: action="store_true", help="Disable the default FlashInfer all-reduce fast path and fall back to SymmMem / NCCL.", ) + parser.add_argument( + "--disable_fused_allreduce_norm", + action="store_true", + help="Disable the FlashInfer fused all-reduce + residual add + RMSNorm op " + "(post-attention), using separate all-reduce + add + norm instead. " + "Requires the FlashInfer all-reduce path; no effect under tpsp mix mode.", + ) parser.add_argument( "--enable_tpsp_mix_mode", action="store_true", diff --git a/lightllm/server/core/objs/start_args_type.py b/lightllm/server/core/objs/start_args_type.py index 9e92b02e1b..298746b262 100644 --- a/lightllm/server/core/objs/start_args_type.py +++ b/lightllm/server/core/objs/start_args_type.py @@ -105,6 +105,7 @@ class StartArgs: visual_use_proxy_mode: bool = field(default=False) disable_symm_mem_allreduce: bool = field(default=False) disable_flashinfer_allreduce: bool = field(default=False) + disable_fused_allreduce_norm: bool = field(default=False) enable_tpsp_mix_mode: bool = field(default=False) enable_dp_prefill_balance: bool = field(default=False) enable_decode_microbatch_overlap: bool = field(default=False) diff --git a/test/benchmark/static_inference/model_infer.py b/test/benchmark/static_inference/model_infer.py index f2c900af09..910219bc55 100644 --- a/test/benchmark/static_inference/model_infer.py +++ b/test/benchmark/static_inference/model_infer.py @@ -37,7 +37,7 @@ def test_model_inference(args): "graph_max_batch_size": args.graph_max_batch_size, "mem_fraction": args.mem_fraction, "max_req_num": 2048, - "batch_max_tokens": 1024, + "batch_max_tokens": args.batch_max_tokens or 1024, "run_mode": "normal", "max_seq_length": args.max_req_total_len, "disable_cudagraph": args.disable_cudagraph, @@ -218,6 +218,7 @@ def decode( b_req_idx=b_req_idx, b_seq_len=b_seq_len, b_mtp_index=b_mtp_index, + b_position_delta=torch.zeros(batch_size, dtype=torch.int32, device="cpu"), mem_indexes_cpu=mem_indexes, is_prefill=False, multimodal_params=[{"images": [], "audios": []} for _ in range(batch_size)], @@ -406,6 +407,10 @@ def tppart_model_infer(args, model_kvargs, batch_size, input_len, output_len, an torch.cuda.empty_cache() enable_overlap = args.enable_decode_microbatch_overlap or args.enable_prefill_microbatch_overlap + if args.skip_autotune_warmup: + from lightllm.common.basemodel.basemodel import TpPartBaseModel + + TpPartBaseModel._autotune_warmup = lambda self: None model_part, _ = get_model(model_cfg, model_kvargs) rank_id = model_kvargs["rank_id"] diff --git a/test/benchmark/static_inference/test_model.py b/test/benchmark/static_inference/test_model.py index 5b3751bcc3..e272aa9ede 100644 --- a/test/benchmark/static_inference/test_model.py +++ b/test/benchmark/static_inference/test_model.py @@ -40,6 +40,11 @@ def test_model_infer(self): action="store_true", help="Enable torch profiler to profile the model", ) + parser.add_argument( + "--skip_autotune_warmup", + action="store_true", + help="Skip model initialization autotune warmup for static benchmarks.", + ) args = parser.parse_args() set_env_start_args(args) torch.multiprocessing.set_start_method("spawn") diff --git a/unit_tests/distributed/test_fused_allreduce_rmsnorm.py b/unit_tests/distributed/test_fused_allreduce_rmsnorm.py new file mode 100644 index 0000000000..4eaff5b556 --- /dev/null +++ b/unit_tests/distributed/test_fused_allreduce_rmsnorm.py @@ -0,0 +1,90 @@ +"""Numeric check for the FlashInfer fused all_reduce + residual add + RMSNorm op. + +Run on 2 GPUs: + CUDA_VISIBLE_DEVICES=0,1 torchrun --nproc_per_node=2 \ + unit_tests/distributed/test_fused_allreduce_rmsnorm.py + +Asserts the fused op matches plain all_reduce + residual add + RMSNorm (the +fallback path) to within bf16 tolerance, and that the residual is updated +in place. Skips cleanly if FlashInfer disables itself (unsupported GPU/world). +""" +import os +import torch +import torch.distributed as dist + + +def rmsnorm_ref(x, weight, eps): + # Matches lightllm RMSNormWeight._native_forward: fp32 accumulate, cast at end. + x_fp32 = x.to(torch.float32) + var = x_fp32.pow(2).mean(dim=-1, keepdim=True) + return ((x_fp32 * torch.rsqrt(var + eps)) * weight).to(x.dtype) + + +def main(): + local_rank = int(os.environ["LOCAL_RANK"]) + torch.cuda.set_device(local_rank) + dist.init_process_group(backend="nccl") + world_size = dist.get_world_size() + rank = dist.get_rank() + + from lightllm.distributed.flashinfer_all_reduce import FlashInferAllReduce + + cpu_group = dist.new_group(list(range(world_size)), backend="gloo") + fi = FlashInferAllReduce(cpu_group, local_rank) + if fi.disabled: + if rank == 0: + print("SKIP: FlashInferAllReduce disabled on this GPU/world_size") + dist.destroy_process_group() + return + + torch.manual_seed(1234 + rank) # distinct per-rank partials + hidden = 4096 + eps = 1e-6 + dtype = torch.bfloat16 + weight = torch.randn(hidden, dtype=dtype, device="cuda") * 0.1 + 1.0 + # broadcast weight so all ranks share the same gamma + dist.broadcast(weight, src=0) + + max_diffs = [] + for tokens in [1, 8, 32]: + partial = torch.randn(tokens, hidden, dtype=dtype, device="cuda") * 0.1 + residual = torch.randn(tokens, hidden, dtype=dtype, device="cuda") * 0.1 + dist.broadcast(residual, src=0) # residual is shared (post-embedding) across ranks + + if not fi.should_use(partial): + if rank == 0: + print(f"tokens={tokens}: should_use False (unexpected for this size)") + continue + + # reference: plain all_reduce, then add + rmsnorm + summed = partial.clone() + dist.all_reduce(summed, group=None) + ref_residual = residual + summed + ref_norm = rmsnorm_ref(ref_residual, weight, eps) + + # fused + res_fused = residual.clone() + norm_out = torch.empty_like(partial) + fi.all_reduce_fused_add_rmsnorm(partial.clone(), res_fused, weight, eps, norm_out) + + d_res = (res_fused.float() - ref_residual.float()).abs().max().item() + d_norm = (norm_out.float() - ref_norm.float()).abs().max().item() + max_diffs.append((tokens, d_res, d_norm)) + # residual update must be near-exact (just an add over the reduced sum) + assert d_res < 1e-2, f"tokens={tokens} residual mismatch {d_res}" + assert d_norm < 3e-2, f"tokens={tokens} norm mismatch {d_norm}" + + if not max_diffs: + dist.destroy_process_group() + raise RuntimeError("FlashInfer initialized, but no fused all_reduce+add+rmsnorm case executed") + + dist.barrier() + if rank == 0: + for tokens, d_res, d_norm in max_diffs: + print(f"tokens={tokens}: max|d residual|={d_res:.2e} max|d norm|={d_norm:.2e}") + print("PASS: fused all_reduce+add+rmsnorm matches reference") + dist.destroy_process_group() + + +if __name__ == "__main__": + main() diff --git a/unit_tests/models/llama/test_fused_allreduce_norm_compatibility.py b/unit_tests/models/llama/test_fused_allreduce_norm_compatibility.py new file mode 100644 index 0000000000..6fadb426c9 --- /dev/null +++ b/unit_tests/models/llama/test_fused_allreduce_norm_compatibility.py @@ -0,0 +1,24 @@ +import pytest + +from lightllm.models.llama.layer_infer.transformer_layer_infer import LlamaTransformerLayerInfer +from lightllm.models.stablelm.layer_infer.transformer_layer_infer import StablelmTransformerLayerInfer +from lightllm.models.starcoder2.layer_infer.transformer_layer_infer import Starcoder2TransformerLayerInfer + + +@pytest.mark.parametrize( + "layer_cls", + [ + StablelmTransformerLayerInfer, + Starcoder2TransformerLayerInfer, + ], +) +def test_layernorm_descendants_disable_fused_rmsnorm(monkeypatch, layer_cls): + def init_llama(self, layer_num, network_config): + self.network_config_ = network_config + self._enable_fused_ar_add_norm = True + + monkeypatch.setattr(LlamaTransformerLayerInfer, "__init__", init_llama) + + layer = layer_cls(0, {}) + + assert not layer._enable_fused_ar_add_norm diff --git a/unit_tests/models/llama/test_output_reduction.py b/unit_tests/models/llama/test_output_reduction.py new file mode 100644 index 0000000000..85af669986 --- /dev/null +++ b/unit_tests/models/llama/test_output_reduction.py @@ -0,0 +1,78 @@ +from types import SimpleNamespace + +import torch + +from lightllm.common.basemodel.layer_infer.template import transformer_layer_infer_template +from lightllm.models.llama.layer_infer.transformer_layer_infer import LlamaTransformerLayerInfer + + +class _IdentityProjection: + def mm(self, input): + return input + + +def test_get_o_reduces_by_default(): + layer = object.__new__(LlamaTransformerLayerInfer) + layer.tp_o_head_num_ = 1 + layer.head_dim_ = 2 + layer._should_fuse_ar_add_norm = lambda infer_state: True + reduce_calls = [] + layer._tpsp_reduce = lambda input, infer_state: reduce_calls.append(input) or input + + input = torch.ones(1, 1, 2) + infer_state = SimpleNamespace(need_dp_prefill_balance=False) + layer_weight = SimpleNamespace(o_proj=_IdentityProjection()) + + output = layer._get_o(input, infer_state, layer_weight) + + assert output.shape == (1, 2) + assert reduce_calls == [output] + + +def test_get_o_only_defers_when_requested(): + layer = object.__new__(LlamaTransformerLayerInfer) + layer.tp_o_head_num_ = 1 + layer.head_dim_ = 2 + reduce_calls = [] + layer._tpsp_reduce = lambda input, infer_state: reduce_calls.append(input) or input + + input = torch.ones(1, 1, 2) + infer_state = SimpleNamespace(need_dp_prefill_balance=False) + layer_weight = SimpleNamespace(o_proj=_IdentityProjection()) + + output = layer._get_o(input, infer_state, layer_weight, defer_reduction=True) + + assert output.shape == (1, 2) + assert reduce_calls == [] + + +def test_ineligible_fusion_does_not_allocate_norm_output(monkeypatch): + layer = object.__new__(LlamaTransformerLayerInfer) + layer.embed_dim_ = 4 + + def fail_allocation(*_args, **_kwargs): + raise AssertionError("norm_out must not be allocated") + + layer.alloc_tensor = fail_allocation + layer._ffn_norm = lambda input_, _infer_state, _layer_weight: input_.clone() + + reduce_calls = [] + monkeypatch.setattr( + transformer_layer_infer_template, + "all_reduce", + lambda tensor, group: reduce_calls.append((tensor, group)), + ) + + group = SimpleNamespace(flashinfer_reduce=SimpleNamespace(should_use=lambda _tensor: False)) + infer_state = SimpleNamespace(dist_group=group) + layer_weight = SimpleNamespace(ffn_norm_weight_=SimpleNamespace(weight=torch.ones(4))) + o = torch.ones((2, 4)) + residual = torch.full((2, 4), 2.0) + + norm_out = layer._reduce_add_ffn_norm(o, residual, infer_state, layer_weight) + + assert len(reduce_calls) == 1 + assert reduce_calls[0][0].data_ptr() == o.data_ptr() + assert reduce_calls[0][1] is group + torch.testing.assert_close(residual, torch.full((2, 4), 3.0)) + torch.testing.assert_close(norm_out, residual)