mirror of
https://github.com/kvcache-ai/ktransformers.git
synced 2026-10-18 22:55:21 +08:00
aa2cd3072f
* [feat](sft): add K2 MoE SFT backend
* [feat](sft): enable packed KGroup backward for K2 SFT
* [feat](serving): add KT LoRA to SGLang adapter converter
* [test]: cover KT SFT and composite LoRA serving paths
* [perf](bench): add reusable KT E2E benchmark harness
* [test](sft): add K2 MoE AMX and real Kimi load smoke cases
* [perf](bench): add K2 SFT packed backward profile harness
* [test](sglang): cover Qwen3.5 LoRA shape filters
* [perf](sft): add K2 forward profiling logs
* [docs](sft): clarify K2 packed backward flow
* [docs](sft): number K2 backward comment steps
* chore: mirror sap4 loader baseline
* Add K2 SFT profiling ranges
* Add backward NVTX ranges for K2 SFT profiling
* perf(sft): add rank8 LoRA backward vector paths
* perf(sft): reuse down LoRA bprop cache
* [perf]: add K2 RAWINT4 prefill mat-mat dispatch
* [perf] Refactor K2 RAWINT4 prefill accumulation
* [perf]: add blocked RAWINT4 prefill backend
Cherry-picked from be40f8e with local amx.py conflict resolution.
* Optimize blocked RAWINT4 decode vecmul
* perf(sft): checkpoint qj5090 packed backward baseline
* perf(sft): batch expert LoRA backward as matmul
* perf(sft): align K2 base backward with BF16 matmul
* Optimize K2 SFT backward workspace path
* docs(sft): hand off RAWINT4 KGroup build and Kimi configs
* [feat](kt-kernel): add Kimi RAWINT4 LoRA SFT kernels
* [feat](kt-kernel): integrate Kimi RAWINT4 LoRA SFT
* [fix](sft): preserve input gradients through frozen routers
Build the routing graph when either activations or router parameters require gradients, while respecting the ambient grad mode. Keep frozen router parameters frozen and reject detached routing weights whenever gradients are needed.
Correct the existing frozen-router expectation and cover input-gradient equivalence for linear, DeepSeek/Kimi and GLM routing, checkpoint reuse, grad modes and detached-output failures.
Validated 62 routing/shared-expert/checkpoint tests on PyTorch 2.9.1 and a two-step Kimi K2.5 RAWINT4 four-GPU S512 LoRA smoke with per-rank routing-input gradient audits.
* [perf](kt-kernel): accelerate RAWINT4 prefill and backward
Reuse decoded weight columns across prefill rows and prepack BF16 backward coefficients into bounded per-worker panels. Use AMX BF16 tiles when available, retaining the AVX512-BF16 path and frozen base weights.
Balance expert work, parallelize cache copies and router reductions, and correct the signed INT4 -8 dot-product boundary. Add a CPU replay benchmark and forward/backward panel regressions.
Validated on sap4 with Kimi K2.5, TP1/TP2 replay, AMX and AVX-512 multi-GPU training, gradient windows, and saved LoRA artifacts. User-facing clean builds and AMD hardware remain separate validation work.
* [fix](sft): limit default OpenMP pools to the CPU owner
Keep the affinity-visible physical-core budget on rank 0 and default non-owner distributed ranks to one thread. Preserve explicit generic and KT-specific thread overrides, including intentional single-thread settings.
Cover single-process behavior, non-owner ranks, repeated configuration, and explicit overrides. The 109 configuration, activation-policy, and backend-dispatch tests pass.
---------
Co-authored-by: yyj <yyj@qj5090.local>
Co-authored-by: djh <jd4136@columbia.edu>
1011 lines
42 KiB
Python
1011 lines
42 KiB
Python
# Base classes for SFT MoE operations
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""
|
|
SFT (Supervised Fine-Tuning) MoE base classes and buffer management.
|
|
|
|
Provides:
|
|
- KExpertsSFTBuffer: Grow-only shared buffer for forward/backward passes
|
|
- BaseSFTMoEWrapper: Abstract base with concrete buffer management (template method pattern)
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
import math
|
|
import torch
|
|
from typing import Optional, Tuple
|
|
from abc import ABC, abstractmethod
|
|
|
|
from ..experts_base import KExpertsCPUBuffer, _MoEBase
|
|
from .backend import is_fp8_sft_method, is_int8_sft_method, is_rawint4_sft_method
|
|
|
|
|
|
def _supports_authoritative_optimizer_grads(
|
|
method: str,
|
|
num_gpu_experts: int,
|
|
*,
|
|
full_weight_grad: bool = False,
|
|
lora_rank: int = 1,
|
|
) -> bool:
|
|
"""Whether this SFT configuration can use C++-authoritative gradients.
|
|
|
|
BF16 supports both base and LoRA authoritative gradients. Quantized base
|
|
weights are frozen, so INT8/FP8 support the same lifecycle only for pure LoRA.
|
|
"""
|
|
if int(num_gpu_experts) != 0:
|
|
return False
|
|
if method == "AMXBF16_SFT":
|
|
return True
|
|
return (
|
|
(is_int8_sft_method(method) or is_fp8_sft_method(method) or is_rawint4_sft_method(method))
|
|
and not bool(full_weight_grad)
|
|
and int(lora_rank) > 0
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class _AuthoritativeOptimizerGrad:
|
|
"""Stable Parameter-to-C++-gradient binding for one optimizer tensor."""
|
|
|
|
name: str
|
|
parameter: torch.nn.Parameter
|
|
grad_view: torch.Tensor
|
|
metadata: tuple
|
|
|
|
|
|
class _CheckpointCacheState(str, Enum):
|
|
EMPTY = "EMPTY"
|
|
READY = "READY"
|
|
POISONED = "POISONED"
|
|
|
|
|
|
def _authoritative_grad_metadata(tensor: torch.Tensor) -> tuple:
|
|
device_index = tensor.device.index if tensor.device.index is not None else -1
|
|
return (
|
|
tensor.dtype,
|
|
tensor.layout,
|
|
tensor.device.type,
|
|
device_index,
|
|
tuple(tensor.shape),
|
|
tuple(tensor.stride()),
|
|
int(tensor.storage_offset()),
|
|
int(tensor.data_ptr()),
|
|
)
|
|
|
|
|
|
class KExpertsSFTBuffer:
|
|
"""
|
|
CPU buffer management for SFT expert computation.
|
|
|
|
Single grow-only buffer (never shrinks). Callers must use [:qlen] slicing
|
|
since the buffer may be larger than the current batch.
|
|
"""
|
|
|
|
_shared_buffer: Optional["KExpertsSFTBuffer"] = None
|
|
|
|
def __init__(
|
|
self,
|
|
qlen: int,
|
|
hidden_size: int,
|
|
moe_intermediate_size: int,
|
|
num_experts: int,
|
|
num_experts_per_tok: int,
|
|
lora_rank: int,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
):
|
|
self.qlen = qlen
|
|
self.hidden_size = hidden_size
|
|
self.moe_intermediate_size = moe_intermediate_size
|
|
self.num_experts = num_experts
|
|
self.num_experts_per_tok = num_experts_per_tok
|
|
self.lora_rank = lora_rank
|
|
self.dtype = dtype
|
|
|
|
pin_memory = False
|
|
|
|
# Forward buffers
|
|
self.input_cpu = torch.empty((qlen, hidden_size), dtype=dtype, device="cpu", pin_memory=pin_memory)
|
|
self.expert_ids_cpu = torch.empty(
|
|
(qlen, num_experts_per_tok), dtype=torch.int64, device="cpu", pin_memory=pin_memory
|
|
)
|
|
self.weights_cpu = torch.empty(
|
|
(qlen, num_experts_per_tok), dtype=torch.float32, device="cpu", pin_memory=pin_memory
|
|
)
|
|
self.output_cpu = torch.empty((qlen, hidden_size), dtype=dtype, device="cpu", pin_memory=pin_memory)
|
|
|
|
# Backward buffers
|
|
self.grad_output_cpu = torch.empty((qlen, hidden_size), dtype=dtype, device="cpu", pin_memory=pin_memory)
|
|
self.grad_input_cpu = torch.empty((qlen, hidden_size), dtype=dtype, device="cpu", pin_memory=pin_memory)
|
|
self.grad_weights = torch.empty((qlen, num_experts_per_tok), dtype=torch.float32, device="cpu")
|
|
|
|
# Batch size tensor for C++ interface
|
|
self.bsz_tensor = torch.tensor([qlen], dtype=torch.int32, device="cpu")
|
|
|
|
@classmethod
|
|
def get_buffer(
|
|
cls,
|
|
qlen: int,
|
|
hidden_size: int,
|
|
moe_intermediate_size: int,
|
|
num_experts: int,
|
|
num_experts_per_tok: int,
|
|
lora_rank: int,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
) -> "KExpertsSFTBuffer":
|
|
"""Get or grow the single shared buffer. Only reallocates when qlen exceeds capacity."""
|
|
buf = cls._shared_buffer
|
|
if buf is not None and qlen <= buf.qlen:
|
|
return buf
|
|
cls._shared_buffer = cls(
|
|
qlen=qlen,
|
|
hidden_size=hidden_size,
|
|
moe_intermediate_size=moe_intermediate_size,
|
|
num_experts=num_experts,
|
|
num_experts_per_tok=num_experts_per_tok,
|
|
lora_rank=lora_rank,
|
|
dtype=dtype,
|
|
)
|
|
return cls._shared_buffer
|
|
|
|
@classmethod
|
|
def clear_cache(cls) -> None:
|
|
"""Clear the shared buffer."""
|
|
cls._shared_buffer = None
|
|
|
|
|
|
class _SFTForwardBufferView:
|
|
"""Minimal buffer view consumed by AMXSFTMoEWrapper._make_forward_task."""
|
|
|
|
__slots__ = ("bsz_tensor", "expert_ids_cpu", "weights_cpu", "input_cpu", "output_cpu")
|
|
|
|
def __init__(
|
|
self,
|
|
bsz_tensor: torch.Tensor,
|
|
expert_ids_cpu: torch.Tensor,
|
|
weights_cpu: torch.Tensor,
|
|
input_cpu: torch.Tensor,
|
|
output_cpu: torch.Tensor,
|
|
):
|
|
self.bsz_tensor = bsz_tensor
|
|
self.expert_ids_cpu = expert_ids_cpu
|
|
self.weights_cpu = weights_cpu
|
|
self.input_cpu = input_cpu
|
|
self.output_cpu = output_cpu
|
|
|
|
|
|
class BaseSFTMoEWrapper(_MoEBase, ABC):
|
|
"""
|
|
Base class for SFT MoE CPU operations with concrete buffer management.
|
|
|
|
Subclasses implement:
|
|
- _make_forward_task(buffer, save_for_backward) -> C++ task object
|
|
- _make_backward_task(buffer) -> C++ task object
|
|
- load_weights(physical_to_logical_map_cpu)
|
|
- init_lora_weights(...)
|
|
- update_lora_weights()
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
layer_idx: int,
|
|
num_experts: int,
|
|
num_experts_per_tok: int,
|
|
hidden_size: int,
|
|
moe_intermediate_size: int,
|
|
num_gpu_experts: int,
|
|
cpuinfer_threads: int,
|
|
threadpool_count: int,
|
|
weight_path: str,
|
|
chunked_prefill_size: int,
|
|
lora_rank: int = 16,
|
|
lora_alpha: float = 32.0,
|
|
lora_dropout: float = 0.0,
|
|
max_cache_depth: int = 1,
|
|
full_weight_grad: bool = False,
|
|
):
|
|
self.cpu_infer = self._get_cpu_infer(cpuinfer_threads, threadpool_count)
|
|
|
|
self._validate_base_config(
|
|
num_experts=num_experts,
|
|
hidden_size=hidden_size,
|
|
moe_intermediate_size=moe_intermediate_size,
|
|
num_experts_per_tok=num_experts_per_tok,
|
|
)
|
|
self._validate_sft_config(
|
|
lora_rank,
|
|
lora_alpha,
|
|
lora_dropout,
|
|
max_cache_depth,
|
|
full_weight_grad=full_weight_grad,
|
|
)
|
|
|
|
self.layer_idx = layer_idx
|
|
self.num_experts = num_experts
|
|
self.num_experts_per_tok = num_experts_per_tok
|
|
self.hidden_size = hidden_size
|
|
self.moe_intermediate_size = moe_intermediate_size
|
|
self.num_gpu_experts = num_gpu_experts
|
|
self.weight_path = weight_path
|
|
self.chunked_prefill_size = chunked_prefill_size
|
|
self.threadpool_count = threadpool_count
|
|
|
|
self.lora_rank = lora_rank
|
|
self.lora_alpha = lora_alpha
|
|
self.lora_dropout = lora_dropout
|
|
self.lora_scaling = lora_alpha / lora_rank if lora_rank > 0 else 0.0
|
|
self.max_cache_depth = max_cache_depth
|
|
|
|
self._full_weight_grad = full_weight_grad
|
|
|
|
self.gate_lora_a: Optional[torch.Tensor] = None
|
|
self.gate_lora_b: Optional[torch.Tensor] = None
|
|
self.up_lora_a: Optional[torch.Tensor] = None
|
|
self.up_lora_b: Optional[torch.Tensor] = None
|
|
self.down_lora_a: Optional[torch.Tensor] = None
|
|
self.down_lora_b: Optional[torch.Tensor] = None
|
|
|
|
# Base weight parameters for full fine-tuning
|
|
self.gate_proj_buf: Optional[torch.Tensor] = None
|
|
self.up_proj_buf: Optional[torch.Tensor] = None
|
|
self.down_proj_buf: Optional[torch.Tensor] = None
|
|
self.grad_gate_proj_buf: Optional[torch.Tensor] = None
|
|
self.grad_up_proj_buf: Optional[torch.Tensor] = None
|
|
self.grad_down_proj_buf: Optional[torch.Tensor] = None
|
|
|
|
self._weights_loaded: bool = False
|
|
self._lora_initialized: bool = False
|
|
self._cache_depth: int = 0
|
|
self._is_skip_lora: bool = False
|
|
self._base_weights_dirty: bool = False
|
|
# AMXSFTMoEWrapper enables this for supported CPU-only SFT methods.
|
|
# Keeping it false here preserves legacy INT4/SkipLoRA behavior.
|
|
self._uses_authoritative_optimizer_grads: bool = False
|
|
self._init_authoritative_optimizer_grads()
|
|
self.reuse_checkpoint_forward: bool = False
|
|
self._checkpoint_cache_state = _CheckpointCacheState.EMPTY
|
|
self._checkpoint_cache_error: Optional[str] = None
|
|
self._checkpoint_output_cpu: Optional[torch.Tensor] = None
|
|
self._checkpoint_output_qlen: int = 0
|
|
self._backward_repack_pending: bool = False
|
|
|
|
self.moe = None
|
|
|
|
# ========== Authoritative optimizer-gradient lifecycle ==========
|
|
|
|
def _init_authoritative_optimizer_grads(self) -> None:
|
|
self._authoritative_optimizer_grads: list[_AuthoritativeOptimizerGrad] = []
|
|
self._authoritative_grad_submission_pending: bool = False
|
|
self._authoritative_grad_pending_accumulate: bool = False
|
|
|
|
@property
|
|
def authoritative_optimizer_grads(self) -> tuple[_AuthoritativeOptimizerGrad, ...]:
|
|
"""Read-only view of the persistent C++ optimizer-gradient bindings."""
|
|
return tuple(self._authoritative_optimizer_grads)
|
|
|
|
def register_authoritative_optimizer_grad(
|
|
self,
|
|
name: str,
|
|
parameter: torch.nn.Parameter,
|
|
grad_view: torch.Tensor,
|
|
) -> None:
|
|
"""Register a C++-written gradient as the Parameter's sole grad copy."""
|
|
if not self._uses_authoritative_optimizer_grads:
|
|
return
|
|
if not isinstance(parameter, torch.nn.Parameter):
|
|
raise TypeError(f"{name}: expected nn.Parameter, got {type(parameter)!r}")
|
|
if not isinstance(grad_view, torch.Tensor):
|
|
raise TypeError(f"{name}: expected Tensor grad view, got {type(grad_view)!r}")
|
|
if parameter.shape != grad_view.shape:
|
|
raise ValueError(
|
|
f"{name}: Parameter/grad shape mismatch: {tuple(parameter.shape)} != {tuple(grad_view.shape)}"
|
|
)
|
|
if parameter.dtype != grad_view.dtype or parameter.device != grad_view.device:
|
|
raise ValueError(
|
|
f"{name}: Parameter/grad dtype or device mismatch: "
|
|
f"{parameter.dtype}/{parameter.device} != {grad_view.dtype}/{grad_view.device}"
|
|
)
|
|
for entry in self._authoritative_optimizer_grads:
|
|
if entry.parameter is parameter:
|
|
raise RuntimeError(f"{name}: Parameter is already registered as {entry.name}")
|
|
if entry.grad_view is grad_view:
|
|
raise RuntimeError(f"{name}: grad view is already registered as {entry.name}")
|
|
|
|
# Initial state is always a closed optimizer window. The view remains
|
|
# alive in this registry while Parameter.grad is None.
|
|
parameter.grad = None
|
|
self._authoritative_optimizer_grads.append(
|
|
_AuthoritativeOptimizerGrad(
|
|
name=name,
|
|
parameter=parameter,
|
|
grad_view=grad_view,
|
|
metadata=_authoritative_grad_metadata(grad_view),
|
|
)
|
|
)
|
|
|
|
def _validate_authoritative_grad_metadata(self) -> None:
|
|
for entry in self._authoritative_optimizer_grads:
|
|
current = _authoritative_grad_metadata(entry.grad_view)
|
|
if current != entry.metadata:
|
|
raise RuntimeError(
|
|
f"{entry.name}: authoritative grad view metadata changed; "
|
|
"replacing or resizing a C++ gradient buffer is unsupported"
|
|
)
|
|
parameter = entry.parameter
|
|
grad_view = entry.grad_view
|
|
if parameter.shape != grad_view.shape or parameter.dtype != grad_view.dtype:
|
|
raise RuntimeError(f"{entry.name}: Parameter metadata no longer matches its authoritative grad view")
|
|
if parameter.device != grad_view.device:
|
|
raise RuntimeError(f"{entry.name}: Parameter device no longer matches its authoritative grad view")
|
|
|
|
def validate_authoritative_optimizer_grad_state(self) -> str:
|
|
"""Validate aliases and return ``empty``, ``closed``, or ``open``."""
|
|
if not self._uses_authoritative_optimizer_grads or not self._authoritative_optimizer_grads:
|
|
return "empty"
|
|
self._validate_authoritative_grad_metadata()
|
|
|
|
none_count = 0
|
|
alias_count = 0
|
|
for entry in self._authoritative_optimizer_grads:
|
|
grad = entry.parameter.grad
|
|
if grad is None:
|
|
none_count += 1
|
|
elif grad is entry.grad_view:
|
|
alias_count += 1
|
|
else:
|
|
raise RuntimeError(
|
|
f"{entry.name}: Parameter.grad was externally replaced; "
|
|
"expected None or the registered authoritative grad view"
|
|
)
|
|
|
|
total = len(self._authoritative_optimizer_grads)
|
|
if none_count == total:
|
|
return "closed"
|
|
if alias_count == total:
|
|
return "open"
|
|
raise RuntimeError(
|
|
"Mixed authoritative optimizer-gradient state: some KT Parameter.grad values are None "
|
|
"while others still alias their C++ buffers"
|
|
)
|
|
|
|
def _prepare_authoritative_optimizer_grad_write(self, optimizer_grad_scale: float) -> bool:
|
|
if self._authoritative_grad_submission_pending:
|
|
raise RuntimeError("An authoritative optimizer-gradient backward submission is already pending")
|
|
scale = float(optimizer_grad_scale)
|
|
if not math.isfinite(scale) or scale <= 0.0:
|
|
raise ValueError(f"optimizer_grad_scale must be finite and positive, got {optimizer_grad_scale}")
|
|
state = self.validate_authoritative_optimizer_grad_state()
|
|
accumulate = state == "open"
|
|
self._authoritative_grad_submission_pending = True
|
|
self._authoritative_grad_pending_accumulate = accumulate
|
|
return accumulate
|
|
|
|
def _publish_authoritative_optimizer_grads(self) -> None:
|
|
if not self._authoritative_grad_submission_pending:
|
|
raise RuntimeError("No authoritative optimizer-gradient backward submission is pending")
|
|
expected_state = "open" if self._authoritative_grad_pending_accumulate else "closed"
|
|
try:
|
|
state = self.validate_authoritative_optimizer_grad_state()
|
|
if state not in ("empty", expected_state):
|
|
raise RuntimeError(
|
|
f"Authoritative optimizer-gradient state changed during C++ backward: "
|
|
f"expected {expected_state}, got {state}"
|
|
)
|
|
for entry in self._authoritative_optimizer_grads:
|
|
entry.parameter.grad = entry.grad_view
|
|
except Exception:
|
|
self._abort_authoritative_optimizer_grad_write()
|
|
raise
|
|
self._authoritative_grad_submission_pending = False
|
|
self._authoritative_grad_pending_accumulate = False
|
|
|
|
def _abort_authoritative_optimizer_grad_write(self) -> None:
|
|
# A failed C++ task may have partially modified its outputs. Closing
|
|
# the Python window forces the next task down the overwrite/full-init path.
|
|
for entry in self._authoritative_optimizer_grads:
|
|
entry.parameter.grad = None
|
|
self._authoritative_grad_submission_pending = False
|
|
self._authoritative_grad_pending_accumulate = False
|
|
|
|
def release_authoritative_optimizer_grads(self) -> None:
|
|
"""Close the optimizer window after step without touching C++ buffers."""
|
|
if not self._uses_authoritative_optimizer_grads:
|
|
return
|
|
if self._authoritative_grad_submission_pending:
|
|
raise RuntimeError("Cannot release authoritative gradients while C++ backward is pending")
|
|
self.validate_authoritative_optimizer_grad_state()
|
|
for entry in self._authoritative_optimizer_grads:
|
|
entry.parameter.grad = None
|
|
|
|
@staticmethod
|
|
def _validate_sft_config(
|
|
lora_rank: int,
|
|
lora_alpha: float,
|
|
lora_dropout: float,
|
|
max_cache_depth: int,
|
|
full_weight_grad: bool = False,
|
|
) -> None:
|
|
if not full_weight_grad and lora_rank <= 0:
|
|
raise ValueError(
|
|
f"lora_rank must be positive in LoRA mode, got {lora_rank}. "
|
|
"Set kt_train_mode='full' for full fine-tuning."
|
|
)
|
|
if lora_rank > 0 and lora_alpha <= 0:
|
|
raise ValueError(f"lora_alpha must be positive, got {lora_alpha}")
|
|
if not 0.0 <= lora_dropout < 1.0:
|
|
raise ValueError(f"lora_dropout must be in [0, 1), got {lora_dropout}")
|
|
if max_cache_depth <= 0:
|
|
raise ValueError(f"max_cache_depth must be positive, got {max_cache_depth}")
|
|
|
|
# ========== Full weight grad methods ==========
|
|
|
|
def init_full_weight_grad_buffers(
|
|
self, gate_proj: torch.Tensor, up_proj: torch.Tensor, down_proj: torch.Tensor
|
|
) -> None:
|
|
"""Initialize base weight nn.Parameter buffers and gradient buffers for full fine-tuning.
|
|
|
|
Args:
|
|
gate_proj: [num_experts, intermediate_size, hidden_size] BF16 CPU tensor
|
|
up_proj: [num_experts, intermediate_size, hidden_size] BF16 CPU tensor
|
|
down_proj: [num_experts, hidden_size, intermediate_size] BF16 CPU tensor
|
|
"""
|
|
import torch.nn as nn
|
|
|
|
dtype = torch.bfloat16
|
|
E = self.num_experts
|
|
I = self.moe_intermediate_size
|
|
H = self.hidden_size
|
|
|
|
# Create nn.Parameter buffers (optimizer-visible)
|
|
self.gate_proj_buf = nn.Parameter(gate_proj.to(dtype=dtype, device="cpu").contiguous(), requires_grad=True)
|
|
self.up_proj_buf = nn.Parameter(up_proj.to(dtype=dtype, device="cpu").contiguous(), requires_grad=True)
|
|
self.down_proj_buf = nn.Parameter(down_proj.to(dtype=dtype, device="cpu").contiguous(), requires_grad=True)
|
|
|
|
# C++ clears these authoritative gradient buffers before first use.
|
|
self.grad_gate_proj_buf = torch.empty(E, I, H, dtype=dtype, device="cpu")
|
|
self.grad_up_proj_buf = torch.empty(E, I, H, dtype=dtype, device="cpu")
|
|
self.grad_down_proj_buf = torch.empty(E, H, I, dtype=dtype, device="cpu")
|
|
|
|
if self._uses_authoritative_optimizer_grads:
|
|
self.register_authoritative_optimizer_grad("base.gate_proj", self.gate_proj_buf, self.grad_gate_proj_buf)
|
|
self.register_authoritative_optimizer_grad("base.up_proj", self.up_proj_buf, self.grad_up_proj_buf)
|
|
self.register_authoritative_optimizer_grad("base.down_proj", self.down_proj_buf, self.grad_down_proj_buf)
|
|
# Legacy backends leave .grad unset here and return these buffers from
|
|
# KTMoEFunction.backward(), preserving their existing AccumulateGrad path.
|
|
|
|
@abstractmethod
|
|
def update_base_weights(self) -> None:
|
|
"""Sync updated base weight parameters back to C++ kernel after optimizer step."""
|
|
...
|
|
|
|
# ========== Abstract methods for subclasses ==========
|
|
|
|
@abstractmethod
|
|
def _make_forward_task(self, buffer: KExpertsSFTBuffer, save_for_backward: bool):
|
|
"""Construct the C++ forward task object. Backend-specific."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def _make_backward_task(
|
|
self,
|
|
buffer: KExpertsSFTBuffer,
|
|
accumulate_optimizer_grads: bool = False,
|
|
optimizer_grad_scale: float = 1.0,
|
|
):
|
|
"""Construct the C++ backward task object. Backend-specific."""
|
|
...
|
|
|
|
@abstractmethod
|
|
def load_weights(self, physical_to_logical_map_cpu: torch.Tensor) -> None: ...
|
|
|
|
@abstractmethod
|
|
def init_lora_weights(
|
|
self,
|
|
gate_lora_a: torch.Tensor,
|
|
gate_lora_b: torch.Tensor,
|
|
up_lora_a: torch.Tensor,
|
|
up_lora_b: torch.Tensor,
|
|
down_lora_a: torch.Tensor,
|
|
down_lora_b: torch.Tensor,
|
|
grad_gate_lora_a: torch.Tensor,
|
|
grad_gate_lora_b: torch.Tensor,
|
|
grad_up_lora_a: torch.Tensor,
|
|
grad_up_lora_b: torch.Tensor,
|
|
grad_down_lora_a: torch.Tensor,
|
|
grad_down_lora_b: torch.Tensor,
|
|
) -> None: ...
|
|
|
|
@abstractmethod
|
|
def update_lora_weights(self) -> None: ...
|
|
|
|
# ========== Buffer helpers ==========
|
|
|
|
def _get_buffer(self, qlen: int) -> KExpertsSFTBuffer:
|
|
return KExpertsSFTBuffer.get_buffer(
|
|
qlen=qlen,
|
|
hidden_size=self.hidden_size,
|
|
moe_intermediate_size=self.moe_intermediate_size,
|
|
num_experts=self.num_experts,
|
|
num_experts_per_tok=self.num_experts_per_tok,
|
|
lora_rank=self.lora_rank,
|
|
dtype=torch.bfloat16,
|
|
)
|
|
|
|
def _validate_forward_inputs(self, hidden_states: torch.Tensor, expert_ids: torch.Tensor, weights: torch.Tensor):
|
|
if not self._weights_loaded:
|
|
raise RuntimeError("Weights not loaded. Call load_weights() or load_weights_from_tensors() first.")
|
|
# Hybrid mode still requires LoRA buffers even though base gradients are
|
|
# enabled. Only pure Full (lora_rank == 0) may legitimately skip them.
|
|
if self.lora_rank > 0 and not self._lora_initialized and not self._is_skip_lora:
|
|
raise RuntimeError("LoRA weights not initialized. Call init_lora_weights() first.")
|
|
qlen = hidden_states.shape[0]
|
|
if qlen > self.chunked_prefill_size:
|
|
raise ValueError(
|
|
f"qlen ({qlen}) exceeds chunked_prefill_size ({self.chunked_prefill_size}). "
|
|
"Increase chunked_prefill_size or reduce qlen to avoid buffer overrun."
|
|
)
|
|
if expert_ids.shape[0] != qlen or expert_ids.shape[1] != self.num_experts_per_tok:
|
|
raise ValueError(
|
|
f"expert_ids shape {tuple(expert_ids.shape)} must be ({qlen}, {self.num_experts_per_tok})."
|
|
)
|
|
if weights.shape[0] != qlen or weights.shape[1] != self.num_experts_per_tok:
|
|
raise ValueError(f"weights shape {tuple(weights.shape)} must be ({qlen}, {self.num_experts_per_tok}).")
|
|
|
|
def _copy_inputs_to_buffer(
|
|
self,
|
|
buffer: KExpertsSFTBuffer,
|
|
hidden_states: torch.Tensor,
|
|
expert_ids: torch.Tensor,
|
|
weights: torch.Tensor,
|
|
qlen: int,
|
|
) -> torch.device:
|
|
"""Copy inputs to CPU buffer, return input device."""
|
|
input_device = hidden_states.device
|
|
buffer.input_cpu[:qlen].copy_(hidden_states.to(torch.bfloat16), non_blocking=True)
|
|
buffer.expert_ids_cpu[:qlen].copy_(expert_ids.to(torch.int64), non_blocking=True)
|
|
buffer.weights_cpu[:qlen].copy_(weights.to(torch.float32), non_blocking=True)
|
|
buffer.bsz_tensor[0] = qlen
|
|
if input_device.type == "cuda":
|
|
torch.cuda.synchronize(input_device)
|
|
return input_device
|
|
|
|
def _copy_grad_output_to_cpu(self, buffer: KExpertsSFTBuffer, grad_output: torch.Tensor, qlen: int):
|
|
"""Copy grad_output to CPU buffer."""
|
|
input_device = grad_output.device
|
|
if input_device.type == "cuda":
|
|
torch.cuda.synchronize(input_device)
|
|
buffer.grad_output_cpu[:qlen].copy_(grad_output.to(torch.bfloat16))
|
|
|
|
def _return_output(self, buffer: KExpertsSFTBuffer, qlen: int, output_device: Optional[torch.device]):
|
|
if output_device is not None:
|
|
return buffer.output_cpu[:qlen].to(device=output_device, non_blocking=True)
|
|
else:
|
|
return buffer.output_cpu[:qlen].clone()
|
|
|
|
@property
|
|
def checkpoint_cache_state(self) -> _CheckpointCacheState:
|
|
return self._checkpoint_cache_state
|
|
|
|
@property
|
|
def _kt_has_cached_forward(self) -> bool:
|
|
"""Compatibility view; cache state is authoritative."""
|
|
return self._checkpoint_cache_state is _CheckpointCacheState.READY
|
|
|
|
def poison_checkpoint_output(self, error: BaseException | str) -> None:
|
|
self._checkpoint_output_cpu = None
|
|
self._checkpoint_output_qlen = 0
|
|
self._checkpoint_cache_state = _CheckpointCacheState.POISONED
|
|
self._checkpoint_cache_error = str(error)
|
|
|
|
def validate_checkpoint_output(self, qlen: int) -> None:
|
|
state = self._checkpoint_cache_state
|
|
if state is _CheckpointCacheState.POISONED:
|
|
detail = f": {self._checkpoint_cache_error}" if self._checkpoint_cache_error else ""
|
|
raise RuntimeError(f"Checkpoint forward cache is poisoned{detail}")
|
|
if state is not _CheckpointCacheState.READY or self._checkpoint_output_cpu is None:
|
|
raise RuntimeError("No cached checkpoint forward output is available.")
|
|
if int(qlen) != self._checkpoint_output_qlen:
|
|
error = RuntimeError(
|
|
f"Cached checkpoint qlen mismatch: cached={self._checkpoint_output_qlen}, requested={qlen}"
|
|
)
|
|
self.poison_checkpoint_output(error)
|
|
raise error
|
|
|
|
def validate_checkpoint_cache_empty(self) -> None:
|
|
state = self._checkpoint_cache_state
|
|
if state is _CheckpointCacheState.POISONED:
|
|
detail = f": {self._checkpoint_cache_error}" if self._checkpoint_cache_error else ""
|
|
raise RuntimeError(f"Checkpoint forward cache is poisoned{detail}")
|
|
if state is not _CheckpointCacheState.EMPTY or self._checkpoint_output_cpu is not None:
|
|
error = RuntimeError("Checkpoint forward cache is still live before a new first forward")
|
|
self.poison_checkpoint_output(error)
|
|
raise error
|
|
|
|
def cache_checkpoint_output(self, output_cpu: torch.Tensor, qlen: int) -> None:
|
|
try:
|
|
if self._checkpoint_cache_state is _CheckpointCacheState.POISONED:
|
|
self.validate_checkpoint_output(qlen)
|
|
if self._checkpoint_cache_state is not _CheckpointCacheState.EMPTY:
|
|
raise RuntimeError(
|
|
"Cannot replace a live checkpoint forward cache before backward consumes it"
|
|
)
|
|
if output_cpu.device.type != "cpu":
|
|
raise ValueError("checkpoint CPU expert output must reside on CPU")
|
|
if output_cpu.shape[0] < qlen:
|
|
raise ValueError(f"checkpoint output is shorter than qlen: {output_cpu.shape[0]} < {qlen}")
|
|
cached_output = output_cpu[:qlen].contiguous()
|
|
except Exception as exc:
|
|
self.poison_checkpoint_output(exc)
|
|
raise
|
|
self._checkpoint_output_cpu = cached_output
|
|
self._checkpoint_output_qlen = int(qlen)
|
|
self._checkpoint_cache_error = None
|
|
self._checkpoint_cache_state = _CheckpointCacheState.READY
|
|
|
|
def get_checkpoint_output(self, qlen: int, output_device: Optional[torch.device] = None) -> torch.Tensor:
|
|
self.validate_checkpoint_output(qlen)
|
|
output = self._checkpoint_output_cpu
|
|
assert output is not None
|
|
try:
|
|
if output_device is not None:
|
|
return output.to(device=output_device, non_blocking=True)
|
|
return output
|
|
except Exception as exc:
|
|
self.poison_checkpoint_output(exc)
|
|
raise
|
|
|
|
def clear_checkpoint_output(self) -> None:
|
|
self._checkpoint_output_cpu = None
|
|
self._checkpoint_output_qlen = 0
|
|
if self._checkpoint_cache_state is not _CheckpointCacheState.POISONED:
|
|
self._checkpoint_cache_state = _CheckpointCacheState.EMPTY
|
|
self._checkpoint_cache_error = None
|
|
|
|
def _return_grads(self, buffer: KExpertsSFTBuffer, qlen: int, output_device: Optional[torch.device]):
|
|
if output_device is not None:
|
|
grad_input = buffer.grad_input_cpu[:qlen].to(device=output_device, non_blocking=True)
|
|
grad_weights = buffer.grad_weights[:qlen].to(device=output_device, non_blocking=True)
|
|
else:
|
|
grad_input = buffer.grad_input_cpu[:qlen].clone()
|
|
grad_weights = buffer.grad_weights[:qlen].clone()
|
|
return grad_input, grad_weights
|
|
|
|
# ========== Concrete forward/backward ==========
|
|
|
|
def _wait_for_pending_backward_repack(self) -> None:
|
|
if getattr(self, "_backward_repack_pending", False):
|
|
with torch.profiler.record_function("kt.sft.wait_backward_repack"):
|
|
self.wait_backward_repack()
|
|
|
|
def forward(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
expert_ids: torch.Tensor,
|
|
weights: torch.Tensor,
|
|
save_for_backward: bool = True,
|
|
output_device: Optional[torch.device] = None,
|
|
) -> torch.Tensor:
|
|
"""Synchronous forward pass with optional gradient caching."""
|
|
self._validate_forward_inputs(hidden_states, expert_ids, weights)
|
|
qlen = hidden_states.shape[0]
|
|
buffer = self._get_buffer(qlen)
|
|
self._copy_inputs_to_buffer(buffer, hidden_states, expert_ids, weights, qlen)
|
|
|
|
self._wait_for_pending_backward_repack()
|
|
self.cpu_infer.submit(self._make_forward_task(buffer, save_for_backward))
|
|
self.cpu_infer.sync()
|
|
|
|
if save_for_backward and self._cache_depth == 0:
|
|
self._cache_depth += 1
|
|
|
|
return self._return_output(buffer, qlen, output_device)
|
|
|
|
def backward(
|
|
self,
|
|
grad_output: torch.Tensor,
|
|
output_device: Optional[torch.device] = None,
|
|
optimizer_grad_scale: float = 1.0,
|
|
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
"""Backward pass computing grad_input and grad_weights."""
|
|
if self._cache_depth <= 0:
|
|
raise RuntimeError("No forward cache available. Call forward(save_for_backward=True) first.")
|
|
if self._uses_authoritative_optimizer_grads and self._authoritative_grad_submission_pending:
|
|
raise RuntimeError("An authoritative optimizer-gradient backward submission is already pending")
|
|
optimizer_grad_scale = float(optimizer_grad_scale)
|
|
if not math.isfinite(optimizer_grad_scale) or optimizer_grad_scale <= 0.0:
|
|
raise ValueError(f"optimizer_grad_scale must be finite and positive, got {optimizer_grad_scale}")
|
|
|
|
qlen = grad_output.shape[0]
|
|
buffer = self._get_buffer(qlen)
|
|
self._copy_grad_output_to_cpu(buffer, grad_output, qlen)
|
|
|
|
use_authoritative = self._uses_authoritative_optimizer_grads
|
|
accumulate_optimizer_grads = False
|
|
if use_authoritative:
|
|
accumulate_optimizer_grads = self._prepare_authoritative_optimizer_grad_write(optimizer_grad_scale)
|
|
try:
|
|
if use_authoritative:
|
|
backward_task = self._make_backward_task(
|
|
buffer,
|
|
accumulate_optimizer_grads=accumulate_optimizer_grads,
|
|
optimizer_grad_scale=optimizer_grad_scale,
|
|
)
|
|
elif optimizer_grad_scale != 1.0:
|
|
# Legacy backends still let PyTorch AccumulateGrad own GAS
|
|
# accumulation, but their C++ dWeight producer must apply
|
|
# distributed world-size normalization before grad clipping.
|
|
backward_task = self._make_backward_task(
|
|
buffer,
|
|
accumulate_optimizer_grads=False,
|
|
optimizer_grad_scale=optimizer_grad_scale,
|
|
)
|
|
else:
|
|
# Preserve the historical task signature for single-rank
|
|
# legacy backends and older compatible extension builds.
|
|
backward_task = self._make_backward_task(buffer)
|
|
self._wait_for_pending_backward_repack()
|
|
self.cpu_infer.submit(backward_task)
|
|
self.cpu_infer.sync()
|
|
result = self._return_grads(buffer, qlen, output_device)
|
|
if use_authoritative:
|
|
self._publish_authoritative_optimizer_grads()
|
|
except Exception:
|
|
if use_authoritative:
|
|
self._abort_authoritative_optimizer_grad_write()
|
|
# The C++ forward cache may already have been consumed by a
|
|
# partially executed backward. Require a fresh forward before
|
|
# retrying instead of reusing an indeterminate cache entry.
|
|
self._cache_depth = max(0, self._cache_depth - 1)
|
|
raise
|
|
|
|
self._cache_depth -= 1
|
|
return result
|
|
|
|
# ========== Async forward ==========
|
|
|
|
def submit_forward(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
expert_ids: torch.Tensor,
|
|
weights: torch.Tensor,
|
|
save_for_backward: bool = True,
|
|
) -> None:
|
|
"""Submit forward pass asynchronously (non-blocking). Call sync_forward() to get results."""
|
|
self._validate_forward_inputs(hidden_states, expert_ids, weights)
|
|
qlen = hidden_states.shape[0]
|
|
buffer = self._get_buffer(qlen)
|
|
self._copy_inputs_to_buffer(buffer, hidden_states, expert_ids, weights, qlen)
|
|
|
|
self._pending_buffer = buffer
|
|
self._pending_save_for_backward = save_for_backward
|
|
self._pending_qlen = qlen
|
|
|
|
self._wait_for_pending_backward_repack()
|
|
self.cpu_infer.submit(self._make_forward_task(buffer, save_for_backward))
|
|
|
|
def sync_forward(self, output_device: Optional[torch.device] = None) -> torch.Tensor:
|
|
"""Synchronize and retrieve forward results. Must be called after submit_forward()."""
|
|
if not hasattr(self, "_pending_buffer") or self._pending_buffer is None:
|
|
raise RuntimeError("No pending forward. Call submit_forward() first.")
|
|
|
|
self.cpu_infer.sync()
|
|
|
|
buffer = self._pending_buffer
|
|
save_for_backward = self._pending_save_for_backward
|
|
qlen = self._pending_qlen
|
|
|
|
if save_for_backward and self._cache_depth == 0:
|
|
self._cache_depth += 1
|
|
|
|
self._pending_buffer = None
|
|
self._pending_save_for_backward = None
|
|
self._pending_qlen = None
|
|
|
|
return self._return_output(buffer, qlen, output_device)
|
|
|
|
# ========== Inference-only async forward ==========
|
|
|
|
def submit_forward_inference(
|
|
self,
|
|
hidden_states: torch.Tensor,
|
|
expert_ids: torch.Tensor,
|
|
weights: torch.Tensor,
|
|
cuda_stream,
|
|
) -> None:
|
|
"""
|
|
Submit an SFT MoE forward pass for serving.
|
|
|
|
This path mirrors the normal KT inference wrapper: inputs are copied to
|
|
pinned CPU staging buffers, the CPUInfer task is enqueued with the
|
|
caller CUDA stream, and sync_forward_inference() returns a persistent
|
|
GPU output buffer. It deliberately avoids the training-oriented
|
|
torch.cuda.synchronize() in _copy_inputs_to_buffer().
|
|
"""
|
|
if not hasattr(self.cpu_infer, "submit_with_cuda_stream"):
|
|
self.submit_forward(hidden_states, expert_ids, weights, save_for_backward=False)
|
|
self._pending_inference_fallback = True
|
|
self._pending_inference_fallback_device = hidden_states.device
|
|
return
|
|
|
|
self._validate_forward_inputs(hidden_states, expert_ids, weights)
|
|
flat_hidden_states = hidden_states.view(-1, hidden_states.shape[-1])
|
|
|
|
(
|
|
input_tensor_cpu,
|
|
expert_ids_cpu,
|
|
_deferred_expert_ids_cpu,
|
|
weights_cpu,
|
|
output_cpu,
|
|
bsz_tensor_cpu,
|
|
output_gpu,
|
|
) = KExpertsCPUBuffer.get_buffer(flat_hidden_states, self.num_experts_per_tok)
|
|
|
|
current_slot = self.layer_idx % KExpertsCPUBuffer.buffer_depth
|
|
bsz_slot_tensor = bsz_tensor_cpu[current_slot]
|
|
|
|
torch_stream = (
|
|
cuda_stream
|
|
if isinstance(cuda_stream, torch.cuda.Stream)
|
|
else torch.cuda.ExternalStream(cuda_stream, device=flat_hidden_states.device)
|
|
)
|
|
with torch.cuda.stream(torch_stream):
|
|
input_tensor_cpu[current_slot].copy_(flat_hidden_states.to(torch.bfloat16), non_blocking=True)
|
|
expert_ids_cpu[current_slot].copy_(expert_ids.to(torch.int64), non_blocking=True)
|
|
weights_cpu[current_slot].copy_(weights.to(torch.float32), non_blocking=True)
|
|
|
|
buffer_view = _SFTForwardBufferView(
|
|
bsz_tensor=bsz_slot_tensor,
|
|
expert_ids_cpu=expert_ids_cpu[current_slot],
|
|
weights_cpu=weights_cpu[current_slot],
|
|
input_cpu=input_tensor_cpu[current_slot],
|
|
output_cpu=output_cpu[current_slot],
|
|
)
|
|
|
|
self._pending_inference_fallback = False
|
|
self._pending_inference_output_cpu = output_cpu[current_slot]
|
|
self._pending_inference_output_gpu = output_gpu[current_slot]
|
|
|
|
self._wait_for_pending_backward_repack()
|
|
self.cpu_infer.submit_with_cuda_stream(
|
|
cuda_stream,
|
|
self._make_forward_task(buffer_view, save_for_backward=False),
|
|
)
|
|
|
|
def sync_forward_inference(self, cuda_stream) -> torch.Tensor:
|
|
"""
|
|
Synchronize a serving forward submitted by submit_forward_inference().
|
|
|
|
Returns a persistent GPU buffer matching the input batch shape. Consumers
|
|
on the same CUDA stream will naturally wait for the non-blocking D2H/H2D
|
|
staging work ordered through CPUInfer's stream synchronization.
|
|
"""
|
|
if getattr(self, "_pending_inference_fallback", False):
|
|
self._pending_inference_fallback = False
|
|
output_device = getattr(self, "_pending_inference_fallback_device", None)
|
|
self._pending_inference_fallback_device = None
|
|
return self.sync_forward(output_device=output_device)
|
|
|
|
if not hasattr(self, "_pending_inference_output_cpu"):
|
|
raise RuntimeError("No pending inference forward. Call submit_forward_inference() first.")
|
|
|
|
torch_stream = (
|
|
cuda_stream
|
|
if isinstance(cuda_stream, torch.cuda.Stream)
|
|
else torch.cuda.ExternalStream(cuda_stream, device=self._pending_inference_output_gpu.device)
|
|
)
|
|
self.cpu_infer.sync_with_cuda_stream(cuda_stream)
|
|
with torch.cuda.stream(torch_stream):
|
|
self._pending_inference_output_gpu.copy_(self._pending_inference_output_cpu, non_blocking=True)
|
|
output = self._pending_inference_output_gpu
|
|
|
|
del self._pending_inference_output_cpu
|
|
del self._pending_inference_output_gpu
|
|
return output
|
|
|
|
# ========== Async backward ==========
|
|
|
|
def submit_backward_async(
|
|
self,
|
|
grad_output: torch.Tensor,
|
|
output_device: Optional[torch.device] = None,
|
|
optimizer_grad_scale: float = 1.0,
|
|
) -> None:
|
|
"""Submit backward task without waiting. Call sync_backward() for results."""
|
|
if self._cache_depth <= 0:
|
|
raise RuntimeError("No forward cache available. Call forward(save_for_backward=True) first.")
|
|
if self._uses_authoritative_optimizer_grads and self._authoritative_grad_submission_pending:
|
|
raise RuntimeError("An authoritative optimizer-gradient backward submission is already pending")
|
|
optimizer_grad_scale = float(optimizer_grad_scale)
|
|
if not math.isfinite(optimizer_grad_scale) or optimizer_grad_scale <= 0.0:
|
|
raise ValueError(f"optimizer_grad_scale must be finite and positive, got {optimizer_grad_scale}")
|
|
|
|
qlen = grad_output.shape[0]
|
|
buffer = self._get_buffer(qlen)
|
|
self._copy_grad_output_to_cpu(buffer, grad_output, qlen)
|
|
|
|
use_authoritative = self._uses_authoritative_optimizer_grads
|
|
accumulate_optimizer_grads = False
|
|
if use_authoritative:
|
|
accumulate_optimizer_grads = self._prepare_authoritative_optimizer_grad_write(optimizer_grad_scale)
|
|
try:
|
|
if use_authoritative:
|
|
backward_task = self._make_backward_task(
|
|
buffer,
|
|
accumulate_optimizer_grads=accumulate_optimizer_grads,
|
|
optimizer_grad_scale=optimizer_grad_scale,
|
|
)
|
|
elif optimizer_grad_scale != 1.0:
|
|
backward_task = self._make_backward_task(
|
|
buffer,
|
|
accumulate_optimizer_grads=False,
|
|
optimizer_grad_scale=optimizer_grad_scale,
|
|
)
|
|
else:
|
|
backward_task = self._make_backward_task(buffer)
|
|
self._wait_for_pending_backward_repack()
|
|
self.cpu_infer.submit(backward_task)
|
|
except Exception:
|
|
if use_authoritative:
|
|
# submit() is expected to be atomic, but drain defensively in
|
|
# case a backend queued work before reporting an error.
|
|
try:
|
|
self.cpu_infer.sync()
|
|
except Exception:
|
|
pass
|
|
self._abort_authoritative_optimizer_grad_write()
|
|
self._cache_depth = max(0, self._cache_depth - 1)
|
|
self._async_bwd_qlen = None
|
|
self._async_bwd_output_device = None
|
|
self._async_bwd_uses_authoritative = False
|
|
raise
|
|
self._async_bwd_qlen = qlen
|
|
self._async_bwd_output_device = output_device
|
|
self._async_bwd_uses_authoritative = use_authoritative
|
|
|
|
def sync_backward(self) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
"""Wait for async backward and return results."""
|
|
if not hasattr(self, "_async_bwd_qlen") or self._async_bwd_qlen is None:
|
|
raise RuntimeError("No pending backward. Call submit_backward_async() first.")
|
|
|
|
use_authoritative = getattr(self, "_async_bwd_uses_authoritative", False)
|
|
try:
|
|
self.cpu_infer.sync()
|
|
qlen = self._async_bwd_qlen
|
|
output_device = self._async_bwd_output_device
|
|
buffer = self._get_buffer(qlen)
|
|
result = self._return_grads(buffer, qlen, output_device)
|
|
if use_authoritative:
|
|
self._publish_authoritative_optimizer_grads()
|
|
except Exception:
|
|
if use_authoritative:
|
|
self._abort_authoritative_optimizer_grad_write()
|
|
self._cache_depth = max(0, self._cache_depth - 1)
|
|
self._async_bwd_qlen = None
|
|
self._async_bwd_output_device = None
|
|
self._async_bwd_uses_authoritative = False
|
|
raise
|
|
|
|
self._cache_depth -= 1
|
|
self._async_bwd_qlen = None
|
|
self._async_bwd_output_device = None
|
|
self._async_bwd_uses_authoritative = False
|
|
return result
|
|
|
|
# ========== Backward repack (optional, subclasses may override) ==========
|
|
|
|
def submit_backward_repack(self):
|
|
if not self._weights_loaded or self.moe is None:
|
|
return
|
|
if hasattr(self.moe, "submit_backward_repack"):
|
|
self.moe.submit_backward_repack()
|
|
self._backward_repack_pending = True
|
|
|
|
def wait_backward_repack(self):
|
|
if not self._weights_loaded or self.moe is None:
|
|
self._backward_repack_pending = False
|
|
return
|
|
if hasattr(self.moe, "wait_backward_repack"):
|
|
self.moe.wait_backward_repack()
|
|
self._backward_repack_pending = False
|