Skip to content

实际部署的挑战和解决方案

PD分离技术在实际生产环境部署时面临诸多挑战。本节讨论这些挑战及其解决方案,为即将去Mooncake实习的新人提供实战经验。

4.9.1 网络延迟挑战

网络延迟是PD分离架构的主要挑战之一,KV Cache传输延迟直接影响TTFT。

挑战分析

解决方案

python
class NetworkLatencyOptimizer:
   """
   网络延迟优化器
   """

   def __init__(self):
         self.transfer_strategies = {
               'local': LocalTransferStrategy(),
               'rack': RackLocalTransferStrategy(),
               'az': AvailabilityZoneTransferStrategy(),
               'remote': RemoteTransferStrategy(),
         }


   def optimize_placement(
         self,
         prefill_nodes: List[Node],
         decode_nodes: List[Node],
   ) -> PlacementPlan:
         """
         优化节点放置
         策略:
         1. 优先同机架部署
         2. 其次同可用区部署
         3. 避免跨地域部署
         """
       
         #   分析网络拓扑
         topology = self._analyze_network_topology(
               prefill_nodes + decode_nodes
         )

         #   构建亲和图
         affinity_graph = self._build_affinity_graph(topology)

         #   优化配对
         pairs = self._optimize_pairs(
               prefill_nodes,
               decode_nodes,
               affinity_graph
         )
         return PlacementPlan(pairs)


   def select_transfer_strategy(
         self,
         prefill_node: Node,
         decode_node: Node,
   ) -> TransferStrategy:
         """选择最优传输策略"""
         # 确定节点位置关系
         if prefill_node.rack == decode_node.rack:
               #   同机架: NVLink或高速IB
               return self.transfer_strategies['local']
         elif prefill_node.az == decode_node.az:
               #   同可用区: IB/RoCE
               return self.transfer_strategies['az']
         else:
               #   跨可用区: 压缩+异步
               return self.transfer_strategies['remote']

     
# RDMA配置检查脚本
class RDMAConfigurator:
   """RDMA   配置管理器"""
    
   def verify_rdma_setup(self) -> bool:
         """ 验证RDMA配置"""
         checks = {
               'device': self._check_rdma_device(),
               'driver': self._check_driver_version(),
               'port': self._check_port_status(),
               'gid': self._check_gid_config(),
               'performance': self._check_performance(),
         }

         for name, result in checks.items():
               if not result:
                    logger.error(f"RDMA check failed: {name}")
                    return False
         return True


   def _check_rdma_device(self) -> bool:
         """ 检查RDMA设备"""
         try:
               devices = subprocess.check_output(
                    ['ibstat'],
                    text=True
               )
               return 'Mellanox' in devices or 'mlx' in devices
         except:
               return False


   def _check_performance(self) -> bool:
         """  检查RDMA性能"""
         #   运行ib_write_bw测试
         result = subprocess.run(
               ['ib_write_bw', '--report_gbits'],
               capture_output=True,
               text=True,
               timeout=30
         )

         #   解析带宽结果
         bandwidth = self._parse_bandwidth(result.stdout)

         #   检查是否达到预期 (例如 180Gbps for HDR)
         return bandwidth > 180

4.9.2 容错处理

PD分离架构的容错处理比同构部署更复杂,需要处理更多故障场景。

故障场景分析

容错实现

python
class FaultToleranceManager:
   """
   容错管理器
   处理PD分离架构的各种故障场景
   """

   def __init__(self):
         self.node_health = {}
         self.request_states = {}
         self.kv_backup_store = KVBackupStore()


   async def handle_prefill_failure(
         self,
         request: Request,
         failed_node: PrefillNode,
   ): 
         """处理Prefill节点故障"""
         logger.error(f"Prefill node {failed_node.id} failed for request {request.id}")

         # 1.   更新节点状态
         failed_node.mark_unhealthy()


         # 2.   检查请求状态
         if request.state == 'prefilling':
               # 需要重新执行Prefill
               # 选择新的Prefill节点
               new_node = await self._select_backup_prefill_node(request)

               #   重新调度
               await self.reschedule_prefill(request, new_node)


         elif request.state == 'transferring':
               # KV Cache 可能部分传输,需要清理
               await self._cleanup_partial_transfer(request)

               #   重新执行Prefill
               new_node = await self._select_backup_prefill_node(request)
               await self.reschedule_prefill(request, new_node)


   async def handle_transfer_failure(
         self,
         request: Request,
         max_retries: int = 3,
   ) -> bool:       
         """处理传输失败,支持重试"""
         retry_count = 0

         while retry_count < max_retries:
               try:
                #   尝试重新传输
                await self.retry_transfer(request)
                return True
           except TransferError as e:
                retry_count += 1
                logger.warning(
                     f"Transfer retry {retry_count}/{max_retries} failed: {e
                )
                await asyncio.sleep(0.1 * retry_count)   # 指数退避
                    
         #   重试耗尽,选择备用Decode节点
         logger.error(f"Transfer failed after {max_retries} retries")
    
    
         backup_decode = await self._select_backup_decode_node(request)
         if backup_decode:
               await self.redirect_to_decode(request, backup_decode)
               return True
         else:
               #   回退到同构执行
               return await self.fallback_to_homogeneous(request)
             
    async def handle_decode_failure(
        self,request: Request,failed_node: DecodeNode,
        ):
        """ 处理Decode节点故障"""
        logger.error(f"Decode node {failed_node.id} failed for request {request.id}")
        
        # 1.   保存当前状态
        last_token = request.last_generated_token
        generated_tokens = request.generated_tokens
        
         # 2.   从备份恢复KV Cache
         kv_cache = await self.kv_backup_store.retrieve(request.id)
        
         if kv_cache is None:
               #   备份不可用,需要重新执行Prefill
               logger.error(f"KV Cache backup not found for request {request.id}")
               await self.handle_prefill_failure(request, failed_node)
               return
    
         # 3.   选择新的Decode节点
         new_node = await self._select_backup_decode_node(request)
    
         # 4.   恢复执行
         await self.resume_decode(request, new_node, kv_cache, last_token)

    async def _select_backup_prefill_node(
        self,
        request: Request,
    ) -> PrefillNode:
        """ 选择备用Prefill节点"""
        
        #   排除故障节点
        healthy_nodes = [
            n for n in self.prefill_nodes
            if n.is_healthy and n.id != request.failed_node_id
        ]

        #   使用正常的选择逻辑
        selector = PrefillNodeSelector()
        return selector.select_node(request, healthy_nodes)


  #   心跳检测实现
  class HeartbeatMonitor:         
        """心跳检测器"""

        def __init__(self, timeout_seconds: float = 10.0):
              self.timeout = timeout_seconds
              self.last_heartbeats = {}


        async def start_monitoring(self):
              """ 启动心跳监控"""
              while True:
                    await self._check_heartbeats()
                    await asyncio.sleep(1)


        async def _check_heartbeats(self):
              """ 检查心跳状态"""
              now = time.time()

              for node_id, last_time in self.last_heartbeats.items():
                    if now - last_time > self.timeout:
                       #   心跳超时,标记节点故障
                       await self._handle_node_failure(node_id)

        def update_heartbeat(self, node_id: str):
              """ 更新心跳时间"""
              self.last_heartbeats[node_id] = time.time()

4.9.3 扩缩容策略

PD分离架构需要根据负载动态调整Prefill和Decode集群的规模。

扩缩容触发条件

python
class AutoScaler:
   """
   自动扩缩容管理器
   根据负载动态调整集群规模
   """

   def __init__(self):
         #   扩容阈值
         self.scale_up_threshold = {
           'prefill': {
                'queue_depth': 20,     #队列深度超过20                                                   
                'avg_wait_time': 5000, # 平均等待时间超过5秒
                'gpu_utilization': 0.9, # GPU利用率超过90%
           },
           'decode': {
                'itl_p99': 100,          # ITL P99 超过100ms                                                
                'batch_size_ratio': 0.95, # 批大小达到上限95%
                'memory_utilization': 0.85, # 内存利用率超过85%
           }
         }

         #   缩容阈值
         self.scale_down_threshold = {
               'prefill': {
                    'avg_gpu_utilization': 0.3,        # 平均GPU利用率低于30%
                    'queue_depth': 2,                  # 队列深度低于2
               },
               'decode': {
                    'avg_batch_size_ratio': 0.3,   #平均批大小比例低于30%
                                                        
                    'avg_memory_utilization': 0.3, # 平均内存利用率低于30%
               }
         }


   async def evaluate_scaling(self, metrics: ClusterMetrics):
         """  评估是否需要扩缩容"""
       
         #   检查Prefill集群
         prefill_action = self._evaluate_prefill_scaling(metrics.prefill)
         if prefill_action:
               await self.execute_scaling('prefill', prefill_action)


         #   检查Decode集群
         decode_action = self._evaluate_decode_scaling(metrics.decode)
         if decode_action:
               await self.execute_scaling('decode', decode_action)


   def _evaluate_prefill_scaling(
        self,
        metrics: PrefillMetrics,
    ) -> Optional[ScalingAction]:
    """评估Prefill集群扩缩容"""
    
    # 检查扩容条件
    if (metrics.queue_depth > self.scale_up_threshold['prefill']['queue_depth'] and
        metrics.avg_wait_time > self.scale_up_threshold['prefill']['avg_wait_time'] and
        metrics.gpu_utilization > self.scale_up_threshold['prefill']['gpu_utilization']):
        return ScalingAction('scale_up', 1)     # 扩容1个节点
    
    # 检查缩容条件(需要连续5分钟满足)
    if (metrics.avg_gpu_utilization < self.scale_down_threshold['prefill']['avg_gpu_utilization'] and
        metrics.queue_depth < self.scale_down_threshold['prefill']['queue_depth']):
        return ScalingAction('scale_down', 1)   # 缩容1个节点
    return None

         
    async def execute_scaling(
        self,
        cluster_type: str,
        action: ScalingAction,
    ):
        """ 执行扩缩容"""
         if action.action_type == 'scale_up':
           await self._scale_up(cluster_type, action.node_count)
         else:
               await self._scale_down(cluster_type, action.node_count)

    async def _scale_up(self, cluster_type: str, count: int):
        """ 扩容操作"""
        logger.info(f"Scaling up {cluster_type} cluster by {count} nodes")
        
         # 1.   申请新节点
         new_nodes = await self.node_provider.provision_nodes(
               node_type=cluster_type,
               count=count,
         )

         # 2.   初始化节点
         for node in new_nodes:
               await self._initialize_node(node, cluster_type)
    
    
         # 3.   加入集群
         if cluster_type == 'prefill':
               self.prefill_nodes.extend(new_nodes)
         else:
               self.decode_nodes.extend(new_nodes)
    
    
         # 4.   通知调度器
         await self.scheduler.update_node_list(
               cluster_type,
               self.get_all_nodes(cluster_type)
         )


   async def _scale_down(self, cluster_type: str, count: int):
      """缩容操作"""
      logger.info(f"Scaling down {cluster_type} cluster by {count} nodes"


      # 1.   选择待缩容节点(选择负载最低的)
      nodes_to_remove = self._select_nodes_to_remove(cluster_type, count)


      # 2.   迁移请求
      for node in nodes_to_remove:
            await self._migrate_requests(node)


      # 3.   从集群移除
      for node in nodes_to_remove:
            if cluster_type == 'prefill':
               self.prefill_nodes.remove(node)
            else:
               self.decode_nodes.remove(node)


      # 4.   释放节点
      await self.node_provider.release_nodes(nodes_to_remove)

扩缩容配置示例

yaml
# autoscaler-config.yaml
  autoscaler:
    enabled: true
    check_interval: 30s       #   检查间隔
    prefill_cluster:
        min_nodes: 2
        max_nodes: 20
        scale_up:
         cooldown: 60s    #   扩容冷却时间
         threshold:
           queue_depth: 20
           wait_time: 5000ms
           gpu_utilization: 0.9
        scale_down:
         cooldown: 300s   #   缩容冷却时间(更长,避免震荡)
         threshold:
           gpu_utilization: 0.3
           queue_depth: 2


    decode_cluster:
        min_nodes: 4
        max_nodes: 50
        scale_up:
         cooldown: 30s
         threshold:
           itl_p99: 100ms
           batch_size_ratio: 0.95
           memory_utilization: 0.85
        scale_down:
         cooldown: 600s
         threshold:
           batch_size_ratio: 0.3
           memory_utilization: 0.3

4.9.4 部署最佳实践

基于实际生产经验,总结PD分离部署的最佳实践

用心记录,持续成长