File size: 6,978 Bytes
36a88b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""LILO — Lightweight Local Intelligence & Optimizer

Helpers for loading and running LILO-100M on CPU. Variants:
  - lilo-100m (fp16) : fp16 weights (221MB on disk) — fastest, quality gate
                       PPL delta 0.08% vs fp32, greedy outputs 5/5 identical
  - lilo-100m (int8) : INT8 group-wise weights (group=64, 119MB on disk) —
                       smallest/RAM-light, PPL delta 0.32%, greedy 5/5 identical

Both ship patched modeling code (freqs_cis fix + working KV cache) so single-token
decode is cheap. Generate with `generate_lilo()` (explicit KV cache).

CPU speed (measured on 2 vCPU AMD EPYC 7K62 Zen2, AVX2-only, under load):
  fp16  14-22 tok/s at --threads 1   (recommended default)
  int8  9-19 tok/s at --threads 1
More threads HURT on tiny models (sync overhead); scale up only on 8+ cores.
"""

import json
import os
from typing import List, Optional, Sequence

import torch
from safetensors import safe_open
from safetensors.torch import load_file

from transformers import AutoConfig, AutoTokenizer

from configuration_deepseek_v4 import DeepseekV4Config
from modeling_deepseek_v4 import (
    DeepseekV4ForCausalLM,
    QuantizedLinear,
    QuantizedEmbedding,
)


# submodule leaf names that get INT8-quantized in the int8 variant
_LINEAR_TARGETS = (
    "wq_a", "wq_b", "wkv", "wo_a", "wo_b",   # attention projections
    "w1", "w2", "w3",                        # experts (gate/up/down)
    "lm_head",
)


def _swap_quantized(model, quantize_embed=True):
    """Swap nn.Linear -> QuantizedLinear and nn.Embedding -> QuantizedEmbedding.
    Returns number of modules swapped."""
    swapped = 0
    for parent in list(dict(model.named_modules()).values()):
        for name, child in list(parent.named_children()):
            target = None
            if name in _LINEAR_TARGETS and child.__class__.__name__ == "Linear":
                target = QuantizedLinear(child.in_features, child.out_features)
            elif quantize_embed and name == "embed_tokens" and child.__class__.__name__ == "Embedding":
                target = QuantizedEmbedding(child.num_embeddings, child.embedding_dim)
            if target is not None:
                setattr(parent, name, target)
                swapped += 1
    return swapped


def load_lilo(path_or_repo, variant="int8", device="cpu", threads=2, hf_token=None):
    """Load a LILO model. variant: "int8" | "fp16" | "fp32"."""
    path = path_or_repo
    if not os.path.isdir(path):
        from huggingface_hub import snapshot_download
        path = snapshot_download(repo_id=path_or_repo, token=hf_token)

    torch.set_num_threads(threads)

    config = DeepseekV4Config.from_pretrained(path, trust_remote_code=True)
    tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)

    if variant == "int8":
        model = DeepseekV4ForCausalLM(config)
        from safetensors.torch import load_file
        sd = load_file(os.path.join(path, "model.int8.safetensors"))
        with safe_open(os.path.join(path, "model.int8.safetensors"), framework="pt") as _sf:
            emb_dtype = _sf.get_slice("model.embed_tokens.weight").get_dtype()
        n = _swap_quantized(model, quantize_embed=(emb_dtype == "I8"))
        missing, unexpected = model.load_state_dict(sd, strict=False)
        bad_missing = [k for k in missing if not k.startswith("freqs_cis")]
        assert not bad_missing and not unexpected, f"missing={bad_missing[:5]} unexpected={unexpected[:5]}"
        model.eval()
        model.model.get_freqs_cis()
        print(f"[lilo] loaded INT8 variant from {path} ({n} modules swapped, "
              f"{sum(t.numel() for t in sd.values())/1e6:.0f}M params)")
    elif variant == "fp16":
        model = DeepseekV4ForCausalLM(config)
        from safetensors.torch import load_file
        sd = load_file(os.path.join(path, "model.fp16.safetensors"))
        missing, unexpected = model.load_state_dict(sd, strict=False)
        assert not missing and not unexpected, f"missing={missing[:5]} unexpected={unexpected[:5]}"
        model.eval()
        model = model.to(torch.float16)
        model.model.get_freqs_cis()
        print(f"[lilo] loaded FP16 variant from {path}")
    else:
        model = DeepseekV4ForCausalLM.from_pretrained(
            path, trust_remote_code=True, dtype=torch.float32, attn_implementation="eager"
        )
        model.eval()
        model.model.get_freqs_cis()
        print(f"[lilo] loaded FP32 variant from {path}")

    model.to(device)
    return tok, model


@torch.inference_mode()
def generate_lilo(
    model,
    tok,
    prompt_ids: Sequence[int],
    max_new_tokens: int = 200,
    temperature: float = 0.7,
    top_p: float = 0.9,
    repetition_penalty: float = 1.0,
    eos_token_id: int = 1,
    stream: bool = False,
):
    """Greedy/sampling generation with an explicit KV cache.

    Returns (generated_ids list). Repetition penalty is applied on the growing
    sequence (standard formulation)."""
    ids = torch.tensor([list(prompt_ids)], dtype=torch.long)
    generated: List[int] = []
    past = None

    logits = None
    for step in range(max_new_tokens):
        if step == 0:
            # chunked prefill to bound peak memory
            seq = ids
            chunk = 256
            for s in range(0, seq.shape[1], chunk):
                piece = seq[:, s : s + chunk]
                out = model(input_ids=piece, past_key_values=past, use_cache=True)
                past = out.past_key_values
            logits = out.logits[0, -1].float()
        else:
            out = model(
                input_ids=torch.tensor([[generated[-1]]], dtype=torch.long),
                past_key_values=past,
                use_cache=True,
            )
            past = out.past_key_values
            logits = out.logits[0, -1].float()

        if repetition_penalty != 1.0:
            for t in set(generated):
                logits[t] /= repetition_penalty if logits[t] > 0 else 1.0 / repetition_penalty

        if temperature <= 0:
            nxt = int(torch.argmax(logits))
        else:
            probs = torch.softmax(logits / max(temperature, 1e-5), dim=-1)
            if top_p < 1.0:
                sp, si = torch.sort(probs, descending=True)
                cum = torch.cumsum(sp, dim=-1)
                mask = cum - sp > top_p
                sp[mask] = 0.0
                probs = torch.zeros_like(probs).scatter_(0, si, sp)
                probs = probs / probs.sum()
            nxt = int(torch.multinomial(probs, 1))
        generated.append(nxt)
        if stream:
            print(tok.decode([nxt]), end="", flush=True)
        if nxt == eos_token_id:
            break
    return generated


def chat(tok, model, messages, **kw):
    prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
    ids = tok.encode(prompt, add_special_tokens=False)
    gen = generate_lilo(model, tok, ids, **kw)
    return tok.decode(gen, skip_special_tokens=True)