File size: 12,604 Bytes
e1acc15
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
"""Evaluation-only native base plus FP32 low-rank delta. Never merges weights.

TP convention: column/output sharding slices B rows; row/input sharding slices
A columns, with partials reduced by the existing worker. Install once BEFORE
capture. Every trained norm/Markov/confidence tensor is consumed or we refuse.
"""
from __future__ import annotations

from dataclasses import asdict
import hashlib
import json
from pathlib import Path
from typing import Callable, Mapping, Protocol, cast

import torch
from torch import Tensor
from torch.nn import functional as F

from ...native.arms import ArmConfig
from .cache_guard import DependencyGuard
from .model import Config


class NativeLinear(Protocol):
    in_features: int
    out_features: int
    def __call__(self, x: Tensor, out_f32: bool = False, out: Tensor | None = None) -> Tensor: ...
    def nbytes(self) -> int: ...


class DeltaLinear:
    def __init__(self, base: NativeLinear, a: Tensor, b: Tensor):
        if a.ndim != 2 or b.shape != (base.out_features, a.shape[0]) or a.shape[1] != base.in_features:
            raise ValueError("adapter shard shape differs from native base")
        self.base, self.a, self.b = base, a.detach().float().contiguous(), b.detach().float().contiguous()
        self.in_features, self.out_features = base.in_features, base.out_features

    def __call__(self, x: Tensor, out_f32: bool = False, out: Tensor | None = None) -> Tensor:
        # x has already undergone the model's input QDQ. Add BEFORE output
        # rounding and, for row-parallel projections, before the all-reduce.
        value = self.base(x, out_f32=True).float()+F.linear(F.linear(x.float(), self.a), self.b)
        value = value if out_f32 else value.to(x.dtype)
        if out is not None:
            out.copy_(value)
            return out
        return value

    def nbytes(self) -> int:
        return self.base.nbytes()+4*(self.a.numel()+self.b.numel())


class GroupedDelta:
    def __init__(self, weight: Tensor, factors: list[tuple[Tensor, Tensor]]):
        if weight.ndim != 3 or len(factors) != weight.shape[0]:
            raise ValueError("grouped adapter ownership mismatch")
        self.weight, self.factors = weight, factors
        if any(a.ndim != 2 or a.shape[1] != weight.shape[2] or b.shape != (weight.shape[1], a.shape[0]) for a, b in factors):
            raise ValueError("grouped adapter factor shape mismatch")

    def __call__(self, x: Tensor) -> Tensor:
        grouped = x.reshape(x.shape[0], len(self.factors), -1).float()
        base = torch.einsum("tgd,god->tgo", grouped, self.weight.float())
        delta = torch.stack([F.linear(F.linear(grouped[:, g], a), b) for g, (a, b) in enumerate(self.factors)], 1)
        return (base+delta).reshape(x.shape[0], -1).to(x.dtype)


class Stage(Protocol):
    wq_a: NativeLinear
    wq_b: NativeLinear
    wkv: NativeLinear
    wo_b: NativeLinear
    sh_w1: NativeLinear
    sh_w2: NativeLinear
    sh_w3: NativeLinear
    wo_a_n: object
    adapter_wo_a: Callable[[Tensor], Tensor] | None
    shared_pair: object
    attn_norm: Tensor
    ffn_norm: Tensor
    q_norm: Tensor
    kv_norm: Tensor


class Markov(Protocol):
    w1: Tensor
    w2: Tensor
    cache: Tensor
    slot_of_token: Tensor


class Rank(Protocol):
    g0: int
    g1: int
    h0: int
    h1: int
    i0: int
    i1: int
    v0: int
    v1: int
    dev: torch.device
    stages: list[Stage]
    main_norm: Tensor
    norm: Tensor
    conf_w: Tensor
    markov: Markov
    mrows: object
    ring_kv: Tensor
    ring_pos: Tensor


class Worker(Protocol):
    cfg: ArmConfig
    ALL: tuple[int, ...]
    ranks: list[Rank]
    graphs: dict[object, object]
    margin_graphs: dict[object, object]
    insert_graphs: dict[object, object]
    adapter_guard: DependencyGuard


CacheFactory = Callable[[Tensor, Tensor, list[int], int], object]


def install(model: Worker, values: Mapping[str, Tensor], config: Config, rank: int,
            cache_factory: CacheFactory, hot_tokens: list[int]) -> None:
    if (model.cfg.dense != "bf16" or model.cfg.head != "bf16" or model.cfg.combine != "official"
            or not model.cfg.wo_a_qdq or model.cfg.kv_qdq != "official" or model.cfg.dense_kernel != "n1"):
        raise ValueError("adapter evaluation requires native BF16 dense/head and official QDQ/combine")
    if model.graphs or model.margin_graphs or model.insert_graphs or hasattr(model, "adapter_guard"):
        raise ValueError("install a single immutable adapter before graph capture; recreate worker")
    expected: dict[str, tuple[int, ...]] = {}
    projections = {"attn.wq_a": (config.qr, config.dim), "attn.wq_b": (config.heads*config.hd, config.qr),
                   "attn.wkv": (config.hd, config.dim), "attn.wo_b": (config.dim, config.groups*config.outr),
                   "ffn.shared_experts.w1": (config.inter, config.dim), "ffn.shared_experts.w3": (config.inter, config.dim),
                   "ffn.shared_experts.w2": (config.dim, config.inter)}
    def tensor_key(name: str) -> str:
        return name.replace(".", "__")
    expected["t.mtp__0__main_norm__weight"] = (config.dim,)
    for name, shape in {"norm.weight": (config.dim,), "markov_head.embed.weight": (config.vocab, config.markov),
                        "markov_head.head.weight": (config.vocab, config.markov),
                        "confidence_head.proj.weight": (1, config.dim+config.markov)}.items():
        expected["t."+tensor_key(f"mtp.{config.stages-1}."+name)] = shape
    for i in range(config.stages):
        for name, (out, width) in projections.items():
            stem = f"stages.{i}.dense.{tensor_key(name)}."
            expected[stem+"a"], expected[stem+"b"] = (rank, width), (out, rank)
        for group in range(config.groups):
            stem = f"stages.{i}.wo_a.{group}."
            expected[stem+"a"] = (rank, config.heads//config.groups*config.hd)
            expected[stem+"b"] = (config.outr, rank)
        for name, width in (("attn_norm", config.dim), ("ffn_norm", config.dim), ("attn.q_norm", config.qr), ("attn.kv_norm", config.hd)):
            expected[f"stages.{i}.t."+tensor_key(f"mtp.{i}.{name}.weight")] = (width,)
    if set(expected) != set(values) or rank not in (32, 64):
        raise ValueError("adapter trainable coverage/rank mismatch; no ignored tensors")
    for name, shape in expected.items():
        if values[name].shape != shape or not bool(torch.isfinite(values[name]).all()):
            raise ValueError(f"adapter shape/nonfinite mismatch: {name}")
    # Build all replacements before mutation. LoRA never changes any base bytes.
    changes: list[tuple[object, str, object]] = []
    dependencies: dict[str, Tensor] = {}
    for r in model.ALL:
        rk = model.ranks[r]
        def on_device(name: str) -> Tensor:
            tensor = values[name].detach().to(device=rk.dev, dtype=torch.float32).clone().contiguous()
            dependencies[f"{r}/{name}"] = tensor
            return tensor
        def replace(owner: object, attribute: str, name: str, selection: slice | None = None, flatten: bool = False) -> None:
            t = on_device(name)
            if selection is not None:
                t = t[selection].contiguous()
            if flatten:
                t = t.flatten()
            changes.append((owner, attribute, t))
        replace(rk, "main_norm", "t.mtp__0__main_norm__weight")
        last = "t."+tensor_key(f"mtp.{config.stages-1}.")
        replace(rk, "norm", last+"norm__weight")
        replace(rk, "conf_w", last+"confidence_head__proj__weight", flatten=True)
        w1 = on_device(last+"markov_head__embed__weight")
        w2 = on_device(last+"markov_head__head__weight")[rk.v0:rk.v1].contiguous()
        # Rebuild exact adapted Markov hot rows; discard all original BF16 rows.
        changes.extend(((rk.markov, "w1", w1), (rk.markov, "w2", w2),
                        (rk, "mrows", cache_factory(w1, w2, hot_tokens, model.cfg.markov_rows))))
        for i, stage in enumerate(rk.stages):
            for op, attribute in (("attn.wq_a", "wq_a"), ("attn.wq_b", "wq_b"), ("attn.wkv", "wkv"),
                                  ("attn.wo_b", "wo_b"), ("ffn.shared_experts.w1", "sh_w1"),
                                  ("ffn.shared_experts.w2", "sh_w2"), ("ffn.shared_experts.w3", "sh_w3")):
                stem = f"stages.{i}.dense.{tensor_key(op)}."
                a, b = on_device(stem+"a"), on_device(stem+"b")
                if op == "attn.wq_b":
                    b = b[rk.h0*config.hd:rk.h1*config.hd]
                elif op == "attn.wo_b":
                    a = a[:, rk.g0*config.outr:rk.g1*config.outr]
                elif op in ("ffn.shared_experts.w1", "ffn.shared_experts.w3"):
                    b = b[rk.i0:rk.i1]
                elif op == "ffn.shared_experts.w2":
                    a = a[:, rk.i0:rk.i1]
                changes.append((stage, attribute, DeltaLinear(cast(NativeLinear, getattr(stage, attribute)), a, b)))
            # Native BF16 wo_a is grouped (not a dense matrix over all groups).
            weight = cast(Tensor, getattr(stage.wo_a_n, "w"))
            factors = [(on_device(f"stages.{i}.wo_a.{g}.a"), on_device(f"stages.{i}.wo_a.{g}.b")) for g in range(rk.g0, rk.g1)]
            changes.append((stage, "adapter_wo_a", GroupedDelta(weight.reshape(rk.g1-rk.g0, config.outr, -1), factors)))
            changes.append((stage, "shared_pair", None))  # prevent a fused base-only bypass
            for op, attribute in (("attn_norm", "attn_norm"), ("ffn_norm", "ffn_norm"), ("attn.q_norm", "q_norm"), ("attn.kv_norm", "kv_norm")):
                replace(stage, attribute, f"stages.{i}.t."+tensor_key(f"mtp.{i}.{op}.weight"))
    with torch.no_grad():
        for owner, attribute, value in changes:
            setattr(owner, attribute, value)
    # Resolve installed bindings on each pre-replay check, detecting both
    # in-place updates and replacement of a tensor/factor object.
    def current_dependencies() -> dict[str, Tensor]:
        current = dict(dependencies)
        for i, (owner, attribute, _) in enumerate(changes):
            value = getattr(owner, attribute)
            if isinstance(value, Tensor):
                current[f"installed/{i}"] = value
            elif isinstance(value, DeltaLinear):
                current[f"delta/{i}/a"], current[f"delta/{i}/b"] = value.a, value.b
            elif isinstance(value, GroupedDelta):
                for g, (a, b) in enumerate(value.factors):
                    current[f"group/{i}/{g}/a"], current[f"group/{i}/{g}/b"] = a, b
        return current
    def invalidate() -> None:
        model.graphs.clear()
        model.margin_graphs.clear()
        model.insert_graphs.clear()
        for r in model.ALL:
            rk = model.ranks[r]
            rk.ring_kv.zero_()
            rk.ring_pos.fill_(-1)
            rk.markov.cache.zero_()
            rk.markov.slot_of_token.fill_(-1)
            rk.mrows = None
    # All feature windows are invalidated before the first adapted insertion.
    for r in model.ALL:
        model.ranks[r].ring_kv.zero_()
        model.ranks[r].ring_pos.fill_(-1)
        model.ranks[r].markov.cache.zero_()
        model.ranks[r].markov.slot_of_token.fill_(-1)
    model.adapter_guard = DependencyGuard(current_dependencies(), invalidate, current_dependencies)


def adapted_identity(base: Mapping[str, object], checkpoint_sha256: str, config: Config) -> dict[str, object]:
    from .pilot import sources
    from .data import sha256
    runtime_sources = sources()
    proxy_root = Path(__file__).resolve().parents[2]/"spark/remote_dspark"
    runtime_sources.update({"process_proxy/"+p.name: sha256(p) for p in sorted(proxy_root.iterdir())
                            if p.is_file() and p.suffix in (".py", ".c")})
    def identity(role: str) -> str:
        return hashlib.sha256(json.dumps({"schema": "dsv41.d2.native-adapter.v1", "role": role, "base": dict(base),
            "adapter_sha256": checkpoint_sha256, "config": asdict(config), "runtime_sources": runtime_sources,
            "semantics": "native-base-plus-fp32-delta-before-rounding;immutable;official-QDQ"}, sort_keys=True).encode()).hexdigest()
    return {"schema": "dsv41.d2.adapted-native-profile.v1", "source_receipt": dict(base),
            "native_base_profile": "native-hybrid-head-hot", "arm": "d2-adapted-native", "profile_label": "adapted; not unchanged source-faithful",
            "adapter_sha256": checkpoint_sha256, "draft_head": "official BF16 head with FP32 logits", "drafter_identity": identity("drafter"), "markov_identity": identity("markov")}