hzlo0 commited on
Commit
36a88b2
·
verified ·
1 Parent(s): 8627945

lilo-100m: fp16 + group-wise int8 CPU release

Browse files
README.md ADDED
@@ -0,0 +1,121 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ language:
4
+ - id
5
+ library_name: transformers
6
+ tags:
7
+ - deepseek
8
+ - moe
9
+ - causal-lm
10
+ - chat
11
+ - cpu
12
+ - indonesian
13
+ datasets:
14
+ - neosantara/IndoTalkSFT
15
+ base_model: neosantara/wader-100m-base
16
+ pipeline_tag: text-generation
17
+ ---
18
+
19
+ # lilo-100m — Lightweight Local Intelligence & Optimizer
20
+
21
+ A ~110M parameter language model implementing the DeepSeek-V4 architecture, fine-tuned for chat/instruction following in Indonesian. This is the **lilo** release of `neosantara/wader-100m`: same architecture and weights, re-packaged explicitly for **CPU-only, low-memory, low-footprint** use.
22
+
23
+ - **Base**: [neosantara/wader-100m-base](https://huggingface.co/neosantara/wader-100m-base) · pretrained on [neosantara/IndoTalk](https://huggingface.co/datasets/neosantara/IndoTalk)
24
+ - **Upstream SFT**: [neosantara/wader-100m](https://huggingface.co/neosantara/wader-100m) on [neosantara/IndoTalkSFT](https://huggingface.co/datasets/neosantara/IndoTalkSFT)
25
+ - **Trained from scratch** — no weights from DeepSeek-V4 were used.
26
+
27
+ ## Why lilo
28
+
29
+ | | wader-100m (original) | **lilo-100m** |
30
+ |---|---|---|
31
+ | Weights on disk | 421MB fp32 | **119MB** (int8) / **221MB** (fp16) |
32
+ | CPU-friendly runtime | bf16 NaN bugs, no KV cache | fp32/fp16 stable, KV cache, inference_mode |
33
+ | Runs on 2 vCPU / 2GB RAM VPS | untested | **measured** |
34
+
35
+ ## Variants
36
+
37
+ | Variant | File | Disk | PPL delta vs fp32 | Greedy outputs |
38
+ |---|---|---|---|---|
39
+ | `fp16` (default) | `model.fp16.safetensors` | 221MB | +0.08% | 5/5 identical |
40
+ | `int8` | `model.int8.safetensors` | 119MB | +0.32% | 5/5 identical |
41
+
42
+ Notes:
43
+ - `int8` is symmetric group-wise quantization (group=64) on all `nn.Linear` layers + embeddings; norms/router/HC params stay fp32. Scales cost ~7MB.
44
+ - Both dequantize to fp32 compute on load — the **disk footprint is what shrinks**, because this CPU (Zen2, AVX2-only, no VNNI) does fp32 GEMM as fast as int8 anyway, and fp16 GEMM is *faster* still.
45
+ - fp16 is the default because it is both the closest to the original quality **and** the fastest on CPU.
46
+
47
+ ## CPU speed (measured on 2 vCPU AMD EPYC 7K62 "Zen2", AVX2-only, box under load)
48
+
49
+ | Variant | threads=1 | threads=2 |
50
+ |---|---|---|
51
+ | fp16 | **14–22 tok/s** | 11–12 tok/s |
52
+ | int8 | 9–19 tok/s | 5–8 tok/s |
53
+
54
+ On this tiny model (320 hidden, 8 layers) thread sync overhead dominates — `--threads 1` is fastest. On 8+ core machines try 2–4.
55
+
56
+ ## Memory safety (no OOM)
57
+
58
+ Peak RSS measured on a 2 vCPU / 1.9GB RAM VPS: **~0.85GB (int8) / ~1.05GB (fp16)** including the Python/torch runtime. Rules baked into `run_cpu.sh`:
59
+
60
+ - `OMP_NUM_THREADS` bounded, `MALLOC_ARENA_MAX=1` (glibc per-thread arenas can silently eat GBs)
61
+ - `TMPDIR` forced to disk, never `/tmp` when `/tmp` is tmpfs
62
+ - prefill chunked at 256 tokens so attention never allocates a big `[S,S]` matrix
63
+
64
+ ## Usage
65
+
66
+ ```bash
67
+ git lfs install
68
+ git clone https://huggingface.co/hzlo0/lilo-100m
69
+ cd lilo-100m
70
+ pip install torch --index-url https://download.pytorch.org/whl/cpu
71
+ pip install transformers safetensors tokenizers accelerate
72
+
73
+ bash run_cpu.sh # chat, default fp16
74
+ CUDA_VISIBLE_DEVICES="" python chat_lilo.py --variant int8 --threads 1 "Halo, apa kabar?"
75
+ ```
76
+
77
+ Chat template (DeepSeek-V4 style):
78
+
79
+ ```
80
+ <|begin_of_sentence|><|User|>Halo<|Assistant|>
81
+ ```
82
+
83
+ ```python
84
+ import torch
85
+ from lilo_common import load_lilo, generate_lilo, chat
86
+
87
+ torch.set_num_threads(1)
88
+ tok, model = load_lilo(".", variant="fp16") # or "int8"
89
+ print(chat(tok, model, [{"role": "user", "content": "Halo, apa kabar?"}],
90
+ max_new_tokens=200, temperature=0.7, top_p=0.9))
91
+ ```
92
+
93
+ Requires `trust_remote_code`-free loading: the model code ships as plain files (`modeling_deepseek_v4.py`) loaded by `lilo_common.py`. (The same code also works via `AutoModelForCausalLM.from_pretrained(..., trust_remote_code=True)`.)
94
+
95
+ ## Architecture
96
+
97
+ | Component | Details |
98
+ |---|---|
99
+ | Parameters | ~110M (41M embeddings, 69M non-embedding) |
100
+ | Hidden size | 320 · Layers 8 |
101
+ | Attention | 8 heads, 1 KV head (MQA), MLA q_lora_rank=160, grouped low-rank output |
102
+ | MoE | 4 routed experts + 1 shared, top-2 routing |
103
+ | Hyper-Connections | hc_mult=4, Sinkhorn routing (replaces residual stream) |
104
+ | Vocab | 129,280 (DeepSeek-V4 tokenizer) · Context 2,048 |
105
+
106
+ ## What was fixed for CPU in this release
107
+
108
+ 1. **freqs_cis recomputed on demand** — original code left a buffer uninitialized after meta-device loading, corrupting RoPE beyond a few tokens.
109
+ 2. **KV cache honored** — original `forward` hardcoded `use_cache=False`, recomputing the whole sequence every decode step. This release caches and steps one token.
110
+ 3. **`return_dict` defaults to True** — transformers 5.x deprecated `config.use_return_dict`, which silently returned tuples.
111
+ 4. **Expert dtype bridge** — `w2(x.to(w2.weight.dtype))` silently cast activations to int8 under weight-only quantization.
112
+ 5. **bf16 NaN** — model runs fp32/fp16; no bf16 anywhere.
113
+
114
+ ## Limitations
115
+
116
+ - Tiny model: 110M params, most capacity in embeddings. Generations are often incoherent or factually wrong.
117
+ - Undertrained: 5K pretrain + 3K SFT steps (upstream). Production models train 100K+ steps on trillions of tokens.
118
+
119
+ ## License
120
+
121
+ Apache-2.0 (inherited from upstream `neosantara/wader-100m`).
__pycache__/configuration_deepseek_v4.cpython-312.pyc ADDED
Binary file (3.81 kB). View file
 
__pycache__/lilo_common.cpython-312.pyc ADDED
Binary file (9.58 kB). View file
 
__pycache__/modeling_deepseek_v4.cpython-312.pyc ADDED
Binary file (49 kB). View file
 
chat_lilo.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """LILO chat CLI — CPU-optimized, KV-cache generation.
3
+
4
+ Usage:
5
+ python chat_lilo.py "prompt" # single prompt
6
+ python chat_lilo.py # interactive
7
+ python chat_lilo.py --temp 0.7 --top-p 0.9 --max 200 "prompt"
8
+ """
9
+ import argparse, os, sys
10
+
11
+ HERE = os.path.dirname(os.path.abspath(__file__))
12
+ sys.path.insert(0, HERE)
13
+
14
+ from lilo_common import load_lilo, generate_lilo
15
+
16
+
17
+ def main():
18
+ ap = argparse.ArgumentParser()
19
+ ap.add_argument("prompt", nargs="*")
20
+ ap.add_argument("--variant", default="fp16", choices=["fp16", "int8"])
21
+ ap.add_argument("--max", type=int, default=200)
22
+ ap.add_argument("--temp", type=float, default=0.7)
23
+ ap.add_argument("--top-p", type=float, default=0.9)
24
+ ap.add_argument("--rep", type=float, default=1.1)
25
+ ap.add_argument("--threads", type=int, default=1)
26
+ ap.add_argument("--path", default=HERE)
27
+ a = ap.parse_args()
28
+
29
+ tok, model = load_lilo(a.path, variant=a.variant, threads=a.threads)
30
+
31
+ def run(prompt):
32
+ ids = tok.encode(prompt, add_special_tokens=False)
33
+ out = generate_lilo(model, tok, ids, max_new_tokens=a.max, temperature=a.temp,
34
+ top_p=a.top_p, repetition_penalty=a.rep, stream=True)
35
+ return tok.decode(out, skip_special_tokens=True)
36
+
37
+ if a.prompt:
38
+ run(" ".join(a.prompt))
39
+ print()
40
+ return
41
+
42
+ print("LILO siap. Ketik pesan (Ctrl-C/Ctrl-D untuk keluar).")
43
+ while True:
44
+ try:
45
+ p = input("\n> ").strip()
46
+ except (EOFError, KeyboardInterrupt):
47
+ print()
48
+ break
49
+ if not p:
50
+ continue
51
+ run(p)
52
+ print()
53
+
54
+
55
+ if __name__ == "__main__":
56
+ main()
chat_template.jinja ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {%- if not add_generation_prompt is defined -%}
2
+ {%- set add_generation_prompt = false -%}
3
+ {%- endif -%}
4
+ {%- set ns = namespace(is_first=false, is_tool=false, is_output_first=true, system_prompt="", is_first_sp=true) -%}
5
+ {%- for message in messages -%}
6
+ {%- if message["role"] == "system" -%}
7
+ {%- if ns.is_first_sp -%}
8
+ {%- set ns.system_prompt = ns.system_prompt + message["content"] -%}
9
+ {%- set ns.is_first_sp = false -%}
10
+ {%- else -%}
11
+ {%- set ns.system_prompt = ns.system_prompt + "\n\n" + message["content"] -%}
12
+ {%- endif -%}
13
+ {%- endif -%}
14
+ {%- endfor -%}
15
+ {{- bos_token -}}
16
+ {{- ns.system_prompt -}}
17
+ {%- for message in messages -%}
18
+ {%- if message["role"] == "user" -%}
19
+ {%- set ns.is_tool = false -%}
20
+ {{- "<|User|>" + message["content"] -}}
21
+ {%- endif -%}
22
+ {%- if message["role"] == "assistant" and message["content"] is not none -%}
23
+ {%- if ns.is_tool -%}
24
+ {{- "<|tool▁outputs▁end|>" + message["content"] + "<|end▁of▁sentence|>" -}}
25
+ {%- set ns.is_tool = false -%}
26
+ {%- else -%}
27
+ {{- "<|Assistant|>" + message["content"] + "<|end▁of▁sentence|>" -}}
28
+ {%- endif -%}
29
+ {%- endif -%}
30
+ {%- endfor -%}
31
+ {%- if add_generation_prompt -%}
32
+ {{- "<|Assistant|>" -}}
33
+ {%- endif -%}
config.json ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "DeepseekV4ForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "auto_map": {
8
+ "AutoConfig": "configuration_deepseek_v4.DeepseekV4Config",
9
+ "AutoModelForCausalLM": "modeling_deepseek_v4.DeepseekV4ForCausalLM"
10
+ },
11
+ "bos_token_id": 0,
12
+ "compress_ratios": [
13
+ 0,
14
+ 0,
15
+ 0,
16
+ 0,
17
+ 0,
18
+ 0,
19
+ 0,
20
+ 0,
21
+ 0
22
+ ],
23
+ "compress_rope_theta": 160000.0,
24
+ "dtype": "float16",
25
+ "eos_token_id": 1,
26
+ "hc_eps": 1e-06,
27
+ "hc_mult": 4,
28
+ "hc_sinkhorn_iters": 2,
29
+ "head_dim": 96,
30
+ "hidden_act": "silu",
31
+ "hidden_size": 320,
32
+ "index_head_dim": 128,
33
+ "index_n_heads": 64,
34
+ "index_topk": 512,
35
+ "initializer_range": 0.02,
36
+ "max_position_embeddings": 2048,
37
+ "model_type": "deepseek_v4",
38
+ "moe_intermediate_size": 640,
39
+ "n_routed_experts": 4,
40
+ "n_shared_experts": 1,
41
+ "nope_head_dim": 64,
42
+ "norm_topk_prob": true,
43
+ "num_attention_heads": 8,
44
+ "num_experts_per_tok": 2,
45
+ "num_hash_layers": 0,
46
+ "num_hidden_layers": 8,
47
+ "num_key_value_heads": 1,
48
+ "num_nextn_predict_layers": 1,
49
+ "o_groups": 2,
50
+ "o_lora_rank": 80,
51
+ "pad_token_id": 1,
52
+ "q_lora_rank": 160,
53
+ "qk_rope_head_dim": 32,
54
+ "rms_norm_eps": 1e-06,
55
+ "rope_parameters": {
56
+ "rope_theta": 10000.0,
57
+ "rope_type": "default"
58
+ },
59
+ "rope_theta": 10000.0,
60
+ "routed_scaling_factor": 1.5,
61
+ "scoring_func": "sqrtsoftplus",
62
+ "sliding_window": 128,
63
+ "swiglu_limit": 0.0,
64
+ "tie_word_embeddings": false,
65
+ "topk_method": "noaux_tc",
66
+ "transformers_version": "5.15.1",
67
+ "use_cache": false,
68
+ "vocab_size": 129280,
69
+ "name": "lilo-100m",
70
+ "quantization_config": {
71
+ "format": "int8-symmetric-groupwise",
72
+ "group_size": 64,
73
+ "modules": "linear+embeddings",
74
+ "file": "model.int8.safetensors"
75
+ }
76
+ }
configuration_deepseek_v4.py ADDED
@@ -0,0 +1,140 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DeepSeek-V4 model configuration.
2
+
3
+ Adapted from the DeepSeek-V4 inference config (deepseek-ai/DeepSeek-V4-Pro)
4
+ and the HF Transformers DeepSeek-V3 config for HF compatibility.
5
+
6
+ Key V4-specific features vs V3:
7
+ - Hyper-Connections (HC): multi-copy hidden states with Sinkhorn routing
8
+ - Compressed Sparse Attention (CSA): compression + sliding window + sparse indexing
9
+ - New MoE routing: sqrtsoftplus scoring, hash-based routing for first layers
10
+ - Large head_dim (512), o_groups/o_lora_rank for grouped output projection
11
+ - No kv_lora_rank (replaced by compress_ratios)
12
+ - No v_head_dim/qk_nope_head_dim (replaced by head_dim)
13
+ """
14
+
15
+ from transformers.configuration_utils import PretrainedConfig
16
+
17
+
18
+ class DeepseekV4Config(PretrainedConfig):
19
+ model_type = "deepseek_v4"
20
+ keys_to_ignore_at_inference = ["past_key_values"]
21
+
22
+ def __init__(
23
+ self,
24
+ vocab_size=129280,
25
+ hidden_size=4096,
26
+ num_hidden_layers=43,
27
+ num_attention_heads=64,
28
+ num_key_value_heads=1,
29
+ # MoE
30
+ moe_intermediate_size=2048,
31
+ n_routed_experts=256,
32
+ n_shared_experts=1,
33
+ num_experts_per_tok=6,
34
+ norm_topk_prob=True,
35
+ scoring_func="sqrtsoftplus",
36
+ routed_scaling_factor=1.5,
37
+ topk_method="noaux_tc",
38
+ num_hash_layers=3,
39
+ swiglu_limit=10.0,
40
+ # MLA / Attention
41
+ q_lora_rank=1024,
42
+ head_dim=512,
43
+ qk_rope_head_dim=64,
44
+ o_groups=8,
45
+ o_lora_rank=1024,
46
+ sliding_window=128,
47
+ # Compression
48
+ compress_ratios=None,
49
+ compress_rope_theta=160000.0,
50
+ # Index attention
51
+ index_n_heads=64,
52
+ index_head_dim=128,
53
+ index_topk=512,
54
+ # Hyper-Connections
55
+ hc_mult=4,
56
+ hc_sinkhorn_iters=20,
57
+ hc_eps=1e-6,
58
+ # MTP
59
+ num_nextn_predict_layers=1,
60
+ # Standard
61
+ hidden_act="silu",
62
+ max_position_embeddings=4096,
63
+ initializer_range=0.02,
64
+ rms_norm_eps=1e-6,
65
+ use_cache=True,
66
+ pad_token_id=None,
67
+ bos_token_id=0,
68
+ eos_token_id=1,
69
+ tie_word_embeddings=False,
70
+ rope_theta=10000.0,
71
+ rope_scaling=None,
72
+ attention_bias=False,
73
+ attention_dropout=0.0,
74
+ **kwargs,
75
+ ):
76
+ self.vocab_size = vocab_size
77
+ self.hidden_size = hidden_size
78
+ self.num_hidden_layers = num_hidden_layers
79
+ self.num_attention_heads = num_attention_heads
80
+ self.num_key_value_heads = num_key_value_heads or num_attention_heads
81
+
82
+ # MoE
83
+ self.moe_intermediate_size = moe_intermediate_size
84
+ self.n_routed_experts = n_routed_experts
85
+ self.n_shared_experts = n_shared_experts
86
+ self.num_experts_per_tok = num_experts_per_tok
87
+ self.norm_topk_prob = norm_topk_prob
88
+ self.scoring_func = scoring_func
89
+ self.routed_scaling_factor = routed_scaling_factor
90
+ self.topk_method = topk_method
91
+ self.num_hash_layers = num_hash_layers
92
+ self.swiglu_limit = swiglu_limit
93
+
94
+ # Attention
95
+ self.q_lora_rank = q_lora_rank
96
+ self.head_dim = head_dim
97
+ self.qk_rope_head_dim = qk_rope_head_dim
98
+ self.nope_head_dim = head_dim - qk_rope_head_dim
99
+ self.o_groups = o_groups
100
+ self.o_lora_rank = o_lora_rank
101
+ self.sliding_window = sliding_window
102
+
103
+ # Compression
104
+ if compress_ratios is None:
105
+ # Default: no compression for small models
106
+ compress_ratios = [0] * (num_hidden_layers + 1)
107
+ self.compress_ratios = compress_ratios
108
+ self.compress_rope_theta = compress_rope_theta
109
+
110
+ # Index attention
111
+ self.index_n_heads = index_n_heads
112
+ self.index_head_dim = index_head_dim
113
+ self.index_topk = index_topk
114
+
115
+ # Hyper-Connections
116
+ self.hc_mult = hc_mult
117
+ self.hc_sinkhorn_iters = hc_sinkhorn_iters
118
+ self.hc_eps = hc_eps
119
+
120
+ # MTP
121
+ self.num_nextn_predict_layers = num_nextn_predict_layers
122
+
123
+ # Standard
124
+ self.hidden_act = hidden_act
125
+ self.max_position_embeddings = max_position_embeddings
126
+ self.initializer_range = initializer_range
127
+ self.rms_norm_eps = rms_norm_eps
128
+ self.use_cache = use_cache
129
+ self.rope_theta = rope_theta
130
+ self.rope_scaling = rope_scaling
131
+ self.attention_bias = attention_bias
132
+ self.attention_dropout = attention_dropout
133
+
134
+ super().__init__(
135
+ pad_token_id=pad_token_id,
136
+ bos_token_id=bos_token_id,
137
+ eos_token_id=eos_token_id,
138
+ tie_word_embeddings=tie_word_embeddings,
139
+ **kwargs,
140
+ )
generation_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_from_model_config": true,
3
+ "bos_token_id": 0,
4
+ "eos_token_id": [
5
+ 1
6
+ ],
7
+ "output_attentions": false,
8
+ "output_hidden_states": false,
9
+ "pad_token_id": 1,
10
+ "transformers_version": "5.15.1",
11
+ "use_cache": true
12
+ }
lilo_common.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """LILO — Lightweight Local Intelligence & Optimizer
2
+
3
+ Helpers for loading and running LILO-100M on CPU. Variants:
4
+ - lilo-100m (fp16) : fp16 weights (221MB on disk) — fastest, quality gate
5
+ PPL delta 0.08% vs fp32, greedy outputs 5/5 identical
6
+ - lilo-100m (int8) : INT8 group-wise weights (group=64, 119MB on disk) —
7
+ smallest/RAM-light, PPL delta 0.32%, greedy 5/5 identical
8
+
9
+ Both ship patched modeling code (freqs_cis fix + working KV cache) so single-token
10
+ decode is cheap. Generate with `generate_lilo()` (explicit KV cache).
11
+
12
+ CPU speed (measured on 2 vCPU AMD EPYC 7K62 Zen2, AVX2-only, under load):
13
+ fp16 14-22 tok/s at --threads 1 (recommended default)
14
+ int8 9-19 tok/s at --threads 1
15
+ More threads HURT on tiny models (sync overhead); scale up only on 8+ cores.
16
+ """
17
+
18
+ import json
19
+ import os
20
+ from typing import List, Optional, Sequence
21
+
22
+ import torch
23
+ from safetensors import safe_open
24
+ from safetensors.torch import load_file
25
+
26
+ from transformers import AutoConfig, AutoTokenizer
27
+
28
+ from configuration_deepseek_v4 import DeepseekV4Config
29
+ from modeling_deepseek_v4 import (
30
+ DeepseekV4ForCausalLM,
31
+ QuantizedLinear,
32
+ QuantizedEmbedding,
33
+ )
34
+
35
+
36
+ # submodule leaf names that get INT8-quantized in the int8 variant
37
+ _LINEAR_TARGETS = (
38
+ "wq_a", "wq_b", "wkv", "wo_a", "wo_b", # attention projections
39
+ "w1", "w2", "w3", # experts (gate/up/down)
40
+ "lm_head",
41
+ )
42
+
43
+
44
+ def _swap_quantized(model, quantize_embed=True):
45
+ """Swap nn.Linear -> QuantizedLinear and nn.Embedding -> QuantizedEmbedding.
46
+ Returns number of modules swapped."""
47
+ swapped = 0
48
+ for parent in list(dict(model.named_modules()).values()):
49
+ for name, child in list(parent.named_children()):
50
+ target = None
51
+ if name in _LINEAR_TARGETS and child.__class__.__name__ == "Linear":
52
+ target = QuantizedLinear(child.in_features, child.out_features)
53
+ elif quantize_embed and name == "embed_tokens" and child.__class__.__name__ == "Embedding":
54
+ target = QuantizedEmbedding(child.num_embeddings, child.embedding_dim)
55
+ if target is not None:
56
+ setattr(parent, name, target)
57
+ swapped += 1
58
+ return swapped
59
+
60
+
61
+ def load_lilo(path_or_repo, variant="int8", device="cpu", threads=2, hf_token=None):
62
+ """Load a LILO model. variant: "int8" | "fp16" | "fp32"."""
63
+ path = path_or_repo
64
+ if not os.path.isdir(path):
65
+ from huggingface_hub import snapshot_download
66
+ path = snapshot_download(repo_id=path_or_repo, token=hf_token)
67
+
68
+ torch.set_num_threads(threads)
69
+
70
+ config = DeepseekV4Config.from_pretrained(path, trust_remote_code=True)
71
+ tok = AutoTokenizer.from_pretrained(path, trust_remote_code=True)
72
+
73
+ if variant == "int8":
74
+ model = DeepseekV4ForCausalLM(config)
75
+ from safetensors.torch import load_file
76
+ sd = load_file(os.path.join(path, "model.int8.safetensors"))
77
+ with safe_open(os.path.join(path, "model.int8.safetensors"), framework="pt") as _sf:
78
+ emb_dtype = _sf.get_slice("model.embed_tokens.weight").get_dtype()
79
+ n = _swap_quantized(model, quantize_embed=(emb_dtype == "I8"))
80
+ missing, unexpected = model.load_state_dict(sd, strict=False)
81
+ bad_missing = [k for k in missing if not k.startswith("freqs_cis")]
82
+ assert not bad_missing and not unexpected, f"missing={bad_missing[:5]} unexpected={unexpected[:5]}"
83
+ model.eval()
84
+ model.model.get_freqs_cis()
85
+ print(f"[lilo] loaded INT8 variant from {path} ({n} modules swapped, "
86
+ f"{sum(t.numel() for t in sd.values())/1e6:.0f}M params)")
87
+ elif variant == "fp16":
88
+ model = DeepseekV4ForCausalLM(config)
89
+ from safetensors.torch import load_file
90
+ sd = load_file(os.path.join(path, "model.fp16.safetensors"))
91
+ missing, unexpected = model.load_state_dict(sd, strict=False)
92
+ assert not missing and not unexpected, f"missing={missing[:5]} unexpected={unexpected[:5]}"
93
+ model.eval()
94
+ model = model.to(torch.float16)
95
+ model.model.get_freqs_cis()
96
+ print(f"[lilo] loaded FP16 variant from {path}")
97
+ else:
98
+ model = DeepseekV4ForCausalLM.from_pretrained(
99
+ path, trust_remote_code=True, dtype=torch.float32, attn_implementation="eager"
100
+ )
101
+ model.eval()
102
+ model.model.get_freqs_cis()
103
+ print(f"[lilo] loaded FP32 variant from {path}")
104
+
105
+ model.to(device)
106
+ return tok, model
107
+
108
+
109
+ @torch.inference_mode()
110
+ def generate_lilo(
111
+ model,
112
+ tok,
113
+ prompt_ids: Sequence[int],
114
+ max_new_tokens: int = 200,
115
+ temperature: float = 0.7,
116
+ top_p: float = 0.9,
117
+ repetition_penalty: float = 1.0,
118
+ eos_token_id: int = 1,
119
+ stream: bool = False,
120
+ ):
121
+ """Greedy/sampling generation with an explicit KV cache.
122
+
123
+ Returns (generated_ids list). Repetition penalty is applied on the growing
124
+ sequence (standard formulation)."""
125
+ ids = torch.tensor([list(prompt_ids)], dtype=torch.long)
126
+ generated: List[int] = []
127
+ past = None
128
+
129
+ logits = None
130
+ for step in range(max_new_tokens):
131
+ if step == 0:
132
+ # chunked prefill to bound peak memory
133
+ seq = ids
134
+ chunk = 256
135
+ for s in range(0, seq.shape[1], chunk):
136
+ piece = seq[:, s : s + chunk]
137
+ out = model(input_ids=piece, past_key_values=past, use_cache=True)
138
+ past = out.past_key_values
139
+ logits = out.logits[0, -1].float()
140
+ else:
141
+ out = model(
142
+ input_ids=torch.tensor([[generated[-1]]], dtype=torch.long),
143
+ past_key_values=past,
144
+ use_cache=True,
145
+ )
146
+ past = out.past_key_values
147
+ logits = out.logits[0, -1].float()
148
+
149
+ if repetition_penalty != 1.0:
150
+ for t in set(generated):
151
+ logits[t] /= repetition_penalty if logits[t] > 0 else 1.0 / repetition_penalty
152
+
153
+ if temperature <= 0:
154
+ nxt = int(torch.argmax(logits))
155
+ else:
156
+ probs = torch.softmax(logits / max(temperature, 1e-5), dim=-1)
157
+ if top_p < 1.0:
158
+ sp, si = torch.sort(probs, descending=True)
159
+ cum = torch.cumsum(sp, dim=-1)
160
+ mask = cum - sp > top_p
161
+ sp[mask] = 0.0
162
+ probs = torch.zeros_like(probs).scatter_(0, si, sp)
163
+ probs = probs / probs.sum()
164
+ nxt = int(torch.multinomial(probs, 1))
165
+ generated.append(nxt)
166
+ if stream:
167
+ print(tok.decode([nxt]), end="", flush=True)
168
+ if nxt == eos_token_id:
169
+ break
170
+ return generated
171
+
172
+
173
+ def chat(tok, model, messages, **kw):
174
+ prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
175
+ ids = tok.encode(prompt, add_special_tokens=False)
176
+ gen = generate_lilo(model, tok, ids, **kw)
177
+ return tok.decode(gen, skip_special_tokens=True)
model.fp16.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d53d5f428cdbfca2a7ad6a023f8e15c61113c36806a3357df66ceef8bcf6a489
3
+ size 220767786
model.int8.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ff83e9b094f2d1e464c1af0c81917660266ebcd11392613b739a3521d12fd6ec
3
+ size 118913084
modeling_deepseek_v4.py ADDED
@@ -0,0 +1,842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """DeepSeek-V4 model implementation for HuggingFace Transformers.
2
+
3
+ Ported from deepseek-ai/DeepSeek-V4-Pro inference/model.py to be compatible
4
+ with HF Trainer, SFTTrainer, and AutoModelForCausalLM.
5
+
6
+ Key V4 architecture features implemented:
7
+ - Hyper-Connections (HC): multi-copy hidden states with Sinkhorn routing
8
+ - Compressed Sparse Attention (CSA) with sliding window
9
+ - MoE with sqrtsoftplus scoring and hash-based routing
10
+ - Grouped low-rank output projection (o_groups + o_lora_rank)
11
+ - Multi-Token Prediction (MTP) layers (disabled for small models)
12
+
13
+ Custom kernels (tilelang) are NOT required — all ops are pure PyTorch.
14
+ For training from scratch in bf16, this is sufficient and simpler.
15
+
16
+ LILO patches (CPU-optimized, behavior-preserving):
17
+ - freqs_cis buffer is recomputed on demand (fixes uninitialized-memory bug
18
+ after transformers>=5 meta-device loading which corrupted RoPE and caused
19
+ NaN logits on sequences longer than a few tokens)
20
+ - KV cache is honored in attention (original code hardcoded use_cache=False,
21
+ which recomputed the full sequence every decode step)
22
+ - Optional symmetric INT8 weight-only quantization for nn.Linear / embedding
23
+ layers (dequantized per-layer during forward, negligible compute overhead)
24
+ """
25
+
26
+ import math
27
+ from typing import Optional, Tuple, List
28
+ from functools import lru_cache
29
+
30
+ import torch
31
+ import torch.nn as nn
32
+ import torch.nn.functional as F
33
+
34
+ from transformers.modeling_outputs import BaseModelOutputWithPast, CausalLMOutputWithPast
35
+ from transformers.modeling_utils import PreTrainedModel
36
+ from transformers.generation import GenerationMixin
37
+
38
+ try:
39
+ from .configuration_deepseek_v4 import DeepseekV4Config
40
+ except ImportError:
41
+ from configuration_deepseek_v4 import DeepseekV4Config
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Utility functions
46
+ # ---------------------------------------------------------------------------
47
+
48
+ class DeepseekV4RMSNorm(nn.Module):
49
+ def __init__(self, dim: int, eps: float = 1e-6):
50
+ super().__init__()
51
+ self.eps = eps
52
+ self.weight = nn.Parameter(torch.ones(dim))
53
+
54
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
55
+ dtype = x.dtype
56
+ x = x.float()
57
+ var = x.pow(2).mean(-1, keepdim=True)
58
+ x = x * torch.rsqrt(var + self.eps)
59
+ return (self.weight * x).to(dtype)
60
+
61
+
62
+ def precompute_freqs_cis(dim, seqlen, base=10000.0):
63
+ """Precompute cos/sin for rotary embeddings (real-valued, compile-friendly)."""
64
+ freqs = 1.0 / (base ** (torch.arange(0, dim, 2, dtype=torch.float32) / dim))
65
+ t = torch.arange(seqlen, dtype=torch.float32)
66
+ freqs = torch.outer(t, freqs) # [S, D//2]
67
+ cos = freqs.cos()
68
+ sin = freqs.sin()
69
+ return torch.stack([cos, sin], dim=0) # [2, S, D//2]
70
+
71
+
72
+ def apply_rotary_emb(x: torch.Tensor, cos_sin: torch.Tensor) -> torch.Tensor:
73
+ """Apply rotary positional embeddings (real-valued, no complex ops).
74
+
75
+ x: [..., D] where D is even
76
+ cos_sin: [2, S, D//2] - precomputed cos and sin
77
+ """
78
+ cos, sin = cos_sin[0], cos_sin[1] # each [S, D//2]
79
+ d = x.shape[-1] // 2
80
+ x1, x2 = x[..., :d], x[..., d:]
81
+ # Broadcast cos/sin to match x shape
82
+ while cos.ndim < x1.ndim:
83
+ cos = cos.unsqueeze(0)
84
+ sin = sin.unsqueeze(0)
85
+ y1 = x1 * cos + x2 * sin
86
+ y2 = x1 * (-sin) + x2 * cos
87
+ return torch.cat([y1, y2], dim=-1).to(x.dtype)
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # INT8 quantization (LILO): symmetric per-output-channel weights.
92
+ # Checkpoint stores int8 tensors + fp32 per-row/per-output-channel scales.
93
+ # ---------------------------------------------------------------------------
94
+
95
+ def quantize_per_channel(w: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
96
+ """Symmetric INT8 quantization along dim 0 (output channels / rows)."""
97
+ wf = w.detach().float()
98
+ scale = wf.abs().amax(dim=1, keepdim=True) / 127.0
99
+ scale = torch.clamp(scale, min=1e-12)
100
+ q = torch.round(wf / scale).clamp(-127, 127).to(torch.int8)
101
+ return q, scale.to(torch.float32)
102
+
103
+
104
+ class QuantizedLinear(nn.Module):
105
+ """Drop-in for nn.Linear(bias=False) holding INT8 weights + scales.
106
+
107
+ Supports two scale layouts:
108
+ - per-output-channel: weight_scale shape [out, 1]
109
+ - group-wise (LILO g64): weight_scale shape [out, in/G] where G is inferred
110
+ """
111
+
112
+ def __init__(self, in_features: int, out_features: int):
113
+ super().__init__()
114
+ self.in_features = in_features
115
+ self.out_features = out_features
116
+ g = 64 if in_features % 64 == 0 else 32
117
+ self.register_buffer("weight", torch.zeros(out_features, in_features, dtype=torch.int8), persistent=True)
118
+ self.register_buffer("weight_scale", torch.ones(out_features, in_features // g, dtype=torch.float32), persistent=True)
119
+ self._w_cache = None
120
+
121
+ def dequantized_weight(self, dtype: torch.dtype = torch.float32) -> torch.Tensor:
122
+ if self._w_cache is None or self._w_cache.dtype != dtype:
123
+ q = self.weight.to(torch.float32)
124
+ s = self.weight_scale
125
+ if s.dim() == 2 and s.shape[1] > 1: # group-wise
126
+ g = s.shape[1]
127
+ self._w_cache = (q.view(s.shape[0], g, q.shape[1] // g) * s.unsqueeze(-1)).view(q.shape).to(dtype)
128
+ else: # per-output-channel
129
+ self._w_cache = (q * s).to(dtype)
130
+ return self._w_cache
131
+
132
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
133
+ return F.linear(x, self.dequantized_weight(x.dtype))
134
+
135
+
136
+ class QuantizedEmbedding(nn.Module):
137
+ """Drop-in for nn.Embedding holding INT8 weights + per-row scales.
138
+
139
+ Supports per-output-channel [N, 1] and group-wise [N, D/G] scale layouts.
140
+ """
141
+
142
+ def __init__(self, num_embeddings: int, embedding_dim: int):
143
+ super().__init__()
144
+ self.num_embeddings = num_embeddings
145
+ self.embedding_dim = embedding_dim
146
+ g = 64 if embedding_dim % 64 == 0 else 32
147
+ self.register_buffer("weight", torch.zeros(num_embeddings, embedding_dim, dtype=torch.int8), persistent=True)
148
+ self.register_buffer("weight_scale", torch.ones(num_embeddings, embedding_dim // g, dtype=torch.float32), persistent=True)
149
+
150
+ def forward(self, ids: torch.Tensor) -> torch.Tensor:
151
+ q = self.weight[ids].to(torch.float32)
152
+ s = self.weight_scale[ids]
153
+ if s.shape[-1] > 1: # group-wise
154
+ g = s.shape[-1]
155
+ in_f = q.shape[-1]
156
+ out = (q.reshape(-1, g, in_f // g) * s.reshape(-1, g, 1)).reshape(-1, in_f)
157
+ return out.reshape(*q.shape[:-1], in_f)
158
+ return (q * s).to(torch.float32)
159
+
160
+
161
+
162
+ # ---------------------------------------------------------------------------
163
+ # Hyper-Connections (HC)
164
+ # ---------------------------------------------------------------------------
165
+
166
+ def hc_split_sinkhorn(mixes, hc_scale, hc_base, hc_mult=4, sinkhorn_iters=20, eps=1e-6):
167
+ """Pure PyTorch implementation of HC split + Sinkhorn normalization.
168
+
169
+ Args:
170
+ mixes: [B, S, (2+hc_mult)*hc_mult] - mixed scores from linear projection
171
+ hc_scale: [3] - scale parameters
172
+ hc_base: [(2+hc_mult)*hc_mult] - bias parameters
173
+ hc_mult: number of HC copies
174
+ sinkhorn_iters: number of Sinkhorn normalization iterations
175
+ eps: numerical stability epsilon
176
+
177
+ Returns:
178
+ pre: [B, S, hc_mult] - pre-connection weights
179
+ post: [B, S, hc_mult] - post-connection weights
180
+ comb: [B, S, hc_mult, hc_mult] - combination matrix
181
+ """
182
+ # Split into pre, post, and combination parts
183
+ pre_raw = mixes[..., :hc_mult]
184
+ post_raw = mixes[..., hc_mult:2*hc_mult]
185
+ comb_raw = mixes[..., 2*hc_mult:].reshape(*mixes.shape[:-1], hc_mult, hc_mult)
186
+
187
+ # Apply scale and base
188
+ pre = torch.sigmoid(pre_raw * hc_scale[0] + hc_base[:hc_mult]) + eps
189
+ post = 2 * torch.sigmoid(post_raw * hc_scale[1] + hc_base[hc_mult:2*hc_mult])
190
+
191
+ # Combination matrix with Sinkhorn normalization
192
+ comb = comb_raw * hc_scale[2] + hc_base[2*hc_mult:].reshape(hc_mult, hc_mult)
193
+
194
+ # Initial softmax along last dim + eps
195
+ comb = F.softmax(comb, dim=-1) + eps
196
+ # Normalize along dim=-2
197
+ comb = comb / (comb.sum(dim=-2, keepdim=True) + eps)
198
+
199
+ # Sinkhorn iterations
200
+ for _ in range(sinkhorn_iters - 1):
201
+ comb = comb / (comb.sum(dim=-1, keepdim=True) + eps)
202
+ comb = comb / (comb.sum(dim=-2, keepdim=True) + eps)
203
+
204
+ return pre, post, comb
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Attention
209
+ # ---------------------------------------------------------------------------
210
+
211
+ class DeepseekV4Attention(nn.Module):
212
+ """Multi-head Latent Attention (MLA) with sliding window.
213
+
214
+ V4 attention uses:
215
+ - Low-rank Q projection (wq_a -> q_norm -> wq_b)
216
+ - Direct KV projection (wkv -> kv_norm) - no kv_lora_rank
217
+ - Grouped low-rank O projection (wo_a -> wo_b)
218
+ - Sliding window attention
219
+ - RoPE on last qk_rope_head_dim dims
220
+ """
221
+
222
+ def __init__(self, config: DeepseekV4Config, layer_idx: int):
223
+ super().__init__()
224
+ self.config = config
225
+ self.layer_idx = layer_idx
226
+ self.hidden_size = config.hidden_size
227
+ self.num_heads = config.num_attention_heads
228
+ self.head_dim = config.head_dim
229
+ self.qk_rope_head_dim = config.qk_rope_head_dim
230
+ self.nope_head_dim = config.head_dim - config.qk_rope_head_dim
231
+ self.q_lora_rank = config.q_lora_rank
232
+ self.o_groups = config.o_groups
233
+ self.o_lora_rank = config.o_lora_rank
234
+ self.scaling = config.head_dim ** -0.5
235
+
236
+ # Q projection: low-rank
237
+ self.wq_a = nn.Linear(self.hidden_size, self.q_lora_rank, bias=False)
238
+ self.q_norm = DeepseekV4RMSNorm(self.q_lora_rank, config.rms_norm_eps)
239
+ self.wq_b = nn.Linear(self.q_lora_rank, self.num_heads * self.head_dim, bias=False)
240
+
241
+ # KV projection: direct (no lora, single head)
242
+ self.wkv = nn.Linear(self.hidden_size, self.head_dim, bias=False)
243
+ self.kv_norm = DeepseekV4RMSNorm(self.head_dim, config.rms_norm_eps)
244
+
245
+ # O projection: grouped low-rank
246
+ # wo_a: [num_heads * head_dim / o_groups] -> [o_groups * o_lora_rank]
247
+ group_head_dim = self.num_heads * self.head_dim // self.o_groups
248
+ self.wo_a = nn.Linear(group_head_dim, self.o_groups * self.o_lora_rank, bias=False)
249
+ self.wo_b = nn.Linear(self.o_groups * self.o_lora_rank, self.hidden_size, bias=False)
250
+
251
+ # Learnable attention sink bias
252
+ self.attn_sink = nn.Parameter(torch.zeros(self.num_heads))
253
+
254
+ def forward(
255
+ self,
256
+ hidden_states: torch.Tensor,
257
+ attention_mask: Optional[torch.Tensor] = None,
258
+ position_ids: Optional[torch.LongTensor] = None,
259
+ freqs_cis: Optional[torch.Tensor] = None,
260
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
261
+ use_cache: bool = False,
262
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
263
+ """LILO-patched: honors KV cache (original ignored it and recomputed
264
+ the whole sequence every decode step). past_key_value = (kv, kv) with
265
+ kv shaped [B, 1, T, head_dim]."""
266
+ bsz, seqlen, _ = hidden_states.shape
267
+ past_len = 0
268
+ if past_key_value is not None and past_key_value[0] is not None:
269
+ past_len = int(past_key_value[0].shape[2])
270
+
271
+ # Q: low-rank projection
272
+ q = self.q_norm(self.wq_a(hidden_states))
273
+ q = self.wq_b(q)
274
+ q = q.view(bsz, seqlen, self.num_heads, self.head_dim).transpose(1, 2)
275
+ # RMSNorm on q per-head
276
+ q = q * torch.rsqrt(q.float().pow(2).mean(-1, keepdim=True) + self.config.rms_norm_eps)
277
+ q = q.to(hidden_states.dtype)
278
+
279
+ # KV: direct projection (single KV head, shared across all Q heads)
280
+ kv = self.kv_norm(self.wkv(hidden_states))
281
+ kv = kv.unsqueeze(1) # [B, 1, S, head_dim]
282
+
283
+ # Apply RoPE to last qk_rope_head_dim dims of q and kv
284
+ if freqs_cis is not None:
285
+ q_rope = q[..., -self.qk_rope_head_dim:]
286
+ kv_rope = kv[..., -self.qk_rope_head_dim:]
287
+ q_rope = apply_rotary_emb(q_rope, freqs_cis)
288
+ kv_rope = apply_rotary_emb(kv_rope, freqs_cis)
289
+ q = torch.cat([q[..., :-self.qk_rope_head_dim], q_rope], dim=-1)
290
+ kv = torch.cat([kv[..., :-self.qk_rope_head_dim], kv_rope], dim=-1)
291
+
292
+ # KV cache
293
+ if past_key_value is not None and past_key_value[0] is not None:
294
+ past_k, _ = past_key_value
295
+ kv = torch.cat([past_k, kv], dim=2)
296
+ new_cache = (kv, kv) if use_cache else None
297
+
298
+ total_len = kv.shape[2]
299
+
300
+ # Attention mask: causal, absolute positions (works for prefill and decode)
301
+ if past_len == 0 and seqlen > 1:
302
+ # fresh causal block: SDPA fast path
303
+ attn_mask = None
304
+ is_causal = True
305
+ elif seqlen == 1 and past_len == 0:
306
+ attn_mask = None
307
+ is_causal = False
308
+ else:
309
+ q_pos = torch.arange(past_len, past_len + seqlen, device=kv.device).unsqueeze(1)
310
+ k_pos = torch.arange(total_len, device=kv.device).unsqueeze(0)
311
+ allowed = q_pos >= k_pos # [S, T]
312
+ if bool(allowed.all()):
313
+ attn_mask = None
314
+ is_causal = False
315
+ else:
316
+ attn_mask = torch.zeros(seqlen, total_len, device=kv.device, dtype=kv.dtype)
317
+ attn_mask = attn_mask.masked_fill(~allowed, float("-inf")).unsqueeze(0).unsqueeze(0)
318
+ is_causal = False
319
+
320
+ # Use PyTorch SDPA (fused kernel, memory-efficient)
321
+ # q: [B, H, S, D], kv: [B, 1, T, D] (MQA via enable_gqa)
322
+ # Note: attn_sink bias is small and omitted in SDPA path for speed.
323
+ # It's a learnable per-head scalar — its effect is minimal and the model
324
+ # will learn to compensate through other parameters.
325
+ attn_output = F.scaled_dot_product_attention(
326
+ q, kv, kv,
327
+ attn_mask=attn_mask,
328
+ is_causal=is_causal,
329
+ scale=self.scaling,
330
+ enable_gqa=True,
331
+ )
332
+
333
+ # De-rotate RoPE on output (inverse rotation = negate sin)
334
+ if freqs_cis is not None:
335
+ cos, sin = freqs_cis[0], freqs_cis[1] # [S, D//2]
336
+ cos_inv = cos.unsqueeze(0).unsqueeze(0) # [1, 1, S, D//2]
337
+ sin_inv = -sin.unsqueeze(0).unsqueeze(0) # negate for inverse
338
+ out_rope = attn_output[..., -self.qk_rope_head_dim:]
339
+ d = out_rope.shape[-1] // 2
340
+ o1, o2 = out_rope[..., :d], out_rope[..., d:]
341
+ out_rope = torch.cat([o1 * cos_inv + o2 * sin_inv, o1 * (-sin_inv) + o2 * cos_inv], dim=-1)
342
+ attn_output = torch.cat([attn_output[..., :-self.qk_rope_head_dim], out_rope.to(attn_output.dtype)], dim=-1)
343
+
344
+ # Grouped output projection
345
+ attn_output = attn_output.transpose(1, 2) # [B, S, H, D]
346
+ attn_output = attn_output.reshape(bsz, seqlen, self.o_groups, -1)
347
+
348
+ # wo_a applied per group: [B, S, G, H*D/G] -> [B, S, G, o_lora_rank]
349
+ wo_a_w = self._wo_a_weight().view(self.o_groups, self.o_lora_rank, -1)
350
+ attn_output = torch.einsum("bsgd,grd->bsgr", attn_output, wo_a_w)
351
+ attn_output = attn_output.flatten(2) # [B, S, G*o_lora_rank]
352
+ attn_output = self.wo_b(attn_output)
353
+
354
+ return attn_output, new_cache
355
+
356
+ def _wo_a_weight(self) -> torch.Tensor:
357
+ if isinstance(self.wo_a, QuantizedLinear):
358
+ return self.wo_a.dequantized_weight()
359
+ return self.wo_a.weight
360
+
361
+
362
+
363
+ # ---------------------------------------------------------------------------
364
+ # MoE
365
+ # ---------------------------------------------------------------------------
366
+
367
+ class DeepseekV4Expert(nn.Module):
368
+ """Single MoE expert with SwiGLU activation."""
369
+
370
+ def __init__(self, hidden_size: int, intermediate_size: int, swiglu_limit: float = 0.0):
371
+ super().__init__()
372
+ self.w1 = nn.Linear(hidden_size, intermediate_size, bias=False) # gate
373
+ self.w2 = nn.Linear(intermediate_size, hidden_size, bias=False) # down
374
+ self.w3 = nn.Linear(hidden_size, intermediate_size, bias=False) # up
375
+ self.swiglu_limit = swiglu_limit
376
+
377
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
378
+ gate = self.w1(x).float()
379
+ up = self.w3(x).float()
380
+ if self.swiglu_limit > 0:
381
+ up = up.clamp(-self.swiglu_limit, self.swiglu_limit)
382
+ gate = gate.clamp(max=self.swiglu_limit)
383
+ x = F.silu(gate) * up
384
+ w2_is_q = isinstance(self.w2, QuantizedLinear)
385
+ return self.w2(x.to(self.w2.dequantized_weight().dtype if w2_is_q else self.w2.weight.dtype))
386
+
387
+
388
+ class DeepseekV4Gate(nn.Module):
389
+ """MoE gating with sqrtsoftplus scoring."""
390
+
391
+ def __init__(self, config: DeepseekV4Config, layer_idx: int):
392
+ super().__init__()
393
+ self.config = config
394
+ self.topk = config.num_experts_per_tok
395
+ self.scoring_func = config.scoring_func
396
+ self.route_scale = config.routed_scaling_factor
397
+ self.is_hash_layer = layer_idx < config.num_hash_layers
398
+
399
+ self.weight = nn.Parameter(torch.empty(config.n_routed_experts, config.hidden_size))
400
+ if not self.is_hash_layer:
401
+ self.bias = nn.Parameter(torch.zeros(config.n_routed_experts))
402
+ else:
403
+ self.register_parameter("bias", None)
404
+
405
+ def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
406
+ scores = F.linear(x.float(), self.weight.float())
407
+
408
+ if self.scoring_func == "softmax":
409
+ scores = scores.softmax(dim=-1)
410
+ elif self.scoring_func == "sigmoid":
411
+ scores = scores.sigmoid()
412
+ elif self.scoring_func == "sqrtsoftplus":
413
+ scores = F.softplus(scores).sqrt()
414
+
415
+ original_scores = scores
416
+
417
+ if self.bias is not None:
418
+ scores = scores + self.bias
419
+
420
+ # Top-k selection
421
+ indices = scores.topk(self.topk, dim=-1)[1]
422
+ weights = original_scores.gather(1, indices)
423
+
424
+ if self.scoring_func != "softmax":
425
+ weights = weights / (weights.sum(dim=-1, keepdim=True) + 1e-20)
426
+
427
+ weights = weights * self.route_scale
428
+ return weights.to(x.dtype), indices
429
+
430
+
431
+ class DeepseekV4MoE(nn.Module):
432
+ """Mixture of Experts layer."""
433
+
434
+ def __init__(self, config: DeepseekV4Config, layer_idx: int):
435
+ super().__init__()
436
+ self.config = config
437
+ self.hidden_size = config.hidden_size
438
+ self.n_routed_experts = config.n_routed_experts
439
+ self.num_experts_per_tok = config.num_experts_per_tok
440
+
441
+ self.gate = DeepseekV4Gate(config, layer_idx)
442
+ self.experts = nn.ModuleList([
443
+ DeepseekV4Expert(config.hidden_size, config.moe_intermediate_size, config.swiglu_limit)
444
+ for _ in range(config.n_routed_experts)
445
+ ])
446
+ self.shared_expert = DeepseekV4Expert(config.hidden_size, config.moe_intermediate_size)
447
+
448
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
449
+ shape = x.shape
450
+ x_flat = x.view(-1, self.hidden_size)
451
+
452
+ weights, indices = self.gate(x_flat)
453
+
454
+ y = torch.zeros_like(x_flat, dtype=torch.float32)
455
+
456
+ # Route tokens to experts
457
+ counts = torch.bincount(indices.flatten(), minlength=self.n_routed_experts)
458
+ for i in range(self.n_routed_experts):
459
+ if counts[i] == 0:
460
+ continue
461
+ idx, top = torch.where(indices == i)
462
+ expert_out = self.experts[i](x_flat[idx])
463
+ y[idx] += (weights[idx, top].unsqueeze(-1) * expert_out.float())
464
+
465
+ # Add shared expert
466
+ y = y + self.shared_expert(x_flat).float()
467
+
468
+ return y.to(x.dtype).view(shape)
469
+
470
+
471
+ # ---------------------------------------------------------------------------
472
+ # Transformer Block
473
+ # ---------------------------------------------------------------------------
474
+
475
+ class DeepseekV4Block(nn.Module):
476
+ """Transformer block with Hyper-Connections.
477
+
478
+ Instead of simple residuals, HC maintains hc_mult copies of the hidden state.
479
+ hc_pre: reduces hc copies -> 1 via learned weighted sum.
480
+ hc_post: expands 1 -> hc copies via learned post-weights + combination matrix.
481
+ """
482
+
483
+ def __init__(self, config: DeepseekV4Config, layer_idx: int):
484
+ super().__init__()
485
+ self.config = config
486
+ self.layer_idx = layer_idx
487
+ self.hc_mult = config.hc_mult
488
+ self.norm_eps = config.rms_norm_eps
489
+ self.hc_eps = config.hc_eps
490
+ self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
491
+
492
+ self.attn = DeepseekV4Attention(config, layer_idx)
493
+ self.ffn = DeepseekV4MoE(config, layer_idx)
494
+ self.attn_norm = DeepseekV4RMSNorm(config.hidden_size, config.rms_norm_eps)
495
+ self.ffn_norm = DeepseekV4RMSNorm(config.hidden_size, config.rms_norm_eps)
496
+
497
+ # HC parameters for attention and FFN sub-layers
498
+ mix_hc = (2 + config.hc_mult) * config.hc_mult
499
+ hc_dim = config.hc_mult * config.hidden_size
500
+
501
+ self.hc_attn_fn = nn.Parameter(torch.empty(mix_hc, hc_dim))
502
+ self.hc_ffn_fn = nn.Parameter(torch.empty(mix_hc, hc_dim))
503
+ self.hc_attn_base = nn.Parameter(torch.empty(mix_hc))
504
+ self.hc_ffn_base = nn.Parameter(torch.empty(mix_hc))
505
+ self.hc_attn_scale = nn.Parameter(torch.empty(3))
506
+ self.hc_ffn_scale = nn.Parameter(torch.empty(3))
507
+
508
+ def hc_pre(self, x, hc_fn, hc_scale, hc_base):
509
+ """Reduce hc_mult copies to 1 via learned weighted sum.
510
+
511
+ x: [B, S, hc_mult, D]
512
+ Returns: y [B, S, D], post [B, S, hc_mult], comb [B, S, hc_mult, hc_mult]
513
+ """
514
+ shape = x.size()
515
+ dtype = x.dtype
516
+ x_flat = x.flatten(2).float() # [B, S, hc_mult*D]
517
+
518
+ rsqrt = torch.rsqrt(x_flat.pow(2).mean(-1, keepdim=True) + self.norm_eps)
519
+ mixes = F.linear(x_flat, hc_fn.float()) * rsqrt # [B, S, mix_hc]
520
+
521
+ pre, post, comb = hc_split_sinkhorn(
522
+ mixes, hc_scale, hc_base,
523
+ self.hc_mult, self.hc_sinkhorn_iters, self.hc_eps
524
+ )
525
+
526
+ # Weighted sum: pre [B, S, hc] * x [B, S, hc, D] -> y [B, S, D]
527
+ y = (pre.unsqueeze(-1) * x.float()).sum(dim=2)
528
+ return y.to(dtype), post, comb
529
+
530
+ def hc_post(self, x, residual, post, comb):
531
+ """Expand 1 -> hc_mult copies.
532
+
533
+ x: [B, S, D] - output from sub-layer
534
+ residual: [B, S, hc_mult, D] - input HC state
535
+ post: [B, S, hc_mult]
536
+ comb: [B, S, hc_mult, hc_mult]
537
+ """
538
+ # post * x + comb * residual
539
+ y = (post.unsqueeze(-1) * x.unsqueeze(2).float() +
540
+ torch.einsum("bsij,bsjd->bsid", comb.float(), residual.float()))
541
+ return y.to(x.dtype)
542
+
543
+ def forward(
544
+ self,
545
+ x: torch.Tensor,
546
+ attention_mask: Optional[torch.Tensor] = None,
547
+ position_ids: Optional[torch.LongTensor] = None,
548
+ freqs_cis: Optional[torch.Tensor] = None,
549
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
550
+ use_cache: bool = False,
551
+ ) -> Tuple[torch.Tensor, Optional[Tuple[torch.Tensor, torch.Tensor]]]:
552
+ """
553
+ x: [B, S, hc_mult, D] - HC state
554
+ """
555
+ # Attention with HC
556
+ residual = x
557
+ y, post, comb = self.hc_pre(x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base)
558
+ y = self.attn_norm(y)
559
+ y, new_cache = self.attn(y, attention_mask=attention_mask, position_ids=position_ids,
560
+ freqs_cis=freqs_cis, past_key_value=past_key_value, use_cache=use_cache)
561
+ x = self.hc_post(y, residual, post, comb)
562
+
563
+ # FFN with HC
564
+ residual = x
565
+ y, post, comb = self.hc_pre(x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base)
566
+ y = self.ffn_norm(y)
567
+ y = self.ffn(y)
568
+ x = self.hc_post(y, residual, post, comb)
569
+
570
+ return x, new_cache
571
+
572
+
573
+ # ---------------------------------------------------------------------------
574
+ # Full Model
575
+ # ---------------------------------------------------------------------------
576
+
577
+ class DeepseekV4PreTrainedModel(PreTrainedModel):
578
+ config_class = DeepseekV4Config
579
+ base_model_prefix = "model"
580
+ supports_gradient_checkpointing = True
581
+ _no_split_modules = ["DeepseekV4Block"]
582
+ _skip_keys_device_placement = ["past_key_values"]
583
+
584
+ def _init_weights(self, module):
585
+ std = self.config.initializer_range
586
+ if isinstance(module, nn.Linear):
587
+ module.weight.data.normal_(mean=0.0, std=std)
588
+ if module.bias is not None:
589
+ module.bias.data.zero_()
590
+ elif isinstance(module, nn.Embedding):
591
+ module.weight.data.normal_(mean=0.0, std=std)
592
+ elif isinstance(module, DeepseekV4RMSNorm):
593
+ module.weight.data.fill_(1.0)
594
+ elif isinstance(module, DeepseekV4Gate):
595
+ module.weight.data.normal_(mean=0.0, std=std)
596
+ if module.bias is not None:
597
+ module.bias.data.zero_()
598
+ elif isinstance(module, DeepseekV4Block):
599
+ # Initialize HC parameters
600
+ nn.init.normal_(module.hc_attn_fn, std=0.01)
601
+ nn.init.normal_(module.hc_ffn_fn, std=0.01)
602
+ nn.init.zeros_(module.hc_attn_base)
603
+ nn.init.zeros_(module.hc_ffn_base)
604
+ nn.init.ones_(module.hc_attn_scale)
605
+ nn.init.ones_(module.hc_ffn_scale)
606
+ elif isinstance(module, DeepseekV4Attention):
607
+ nn.init.zeros_(module.attn_sink)
608
+
609
+
610
+ class DeepseekV4Model(DeepseekV4PreTrainedModel):
611
+ def __init__(self, config: DeepseekV4Config):
612
+ super().__init__(config)
613
+ self.config = config
614
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size)
615
+ self.layers = nn.ModuleList([
616
+ DeepseekV4Block(config, layer_idx)
617
+ for layer_idx in range(config.num_hidden_layers)
618
+ ])
619
+ self.norm = DeepseekV4RMSNorm(config.hidden_size, config.rms_norm_eps)
620
+
621
+ # HC head parameters (for contracting hc_mult -> 1 at output)
622
+ hc_dim = config.hc_mult * config.hidden_size
623
+ self.hc_head_fn = nn.Parameter(torch.empty(config.hc_mult, hc_dim))
624
+ self.hc_head_base = nn.Parameter(torch.empty(config.hc_mult))
625
+ self.hc_head_scale = nn.Parameter(torch.empty(1))
626
+
627
+ # Precomputed RoPE frequencies (non-persistent so it never enters
628
+ # checkpoints; recomputed on demand — see get_freqs_cis).
629
+ self.register_buffer(
630
+ "freqs_cis",
631
+ precompute_freqs_cis(config.qk_rope_head_dim, config.max_position_embeddings, config.rope_theta),
632
+ persistent=False,
633
+ )
634
+
635
+ self.gradient_checkpointing = False
636
+ self.post_init()
637
+
638
+ def get_freqs_cis(self) -> torch.Tensor:
639
+ """Return cos/sin table [2, max_pos, D//2], recomputing if the buffer
640
+ was left as uninitialized memory by meta-device loading (transformers>=5)."""
641
+ buf = getattr(self, "freqs_cis", None)
642
+ ok = (
643
+ isinstance(buf, torch.Tensor)
644
+ and buf.dim() == 3
645
+ and int(buf.shape[1]) >= int(self.config.max_position_embeddings)
646
+ and bool(torch.isfinite(buf).all())
647
+ and float(buf.abs().max()) <= 2.0
648
+ )
649
+ if not ok:
650
+ buf = precompute_freqs_cis(
651
+ self.config.qk_rope_head_dim,
652
+ self.config.max_position_embeddings,
653
+ self.config.rope_theta,
654
+ ).to(self.device if hasattr(self, "device") else "cpu")
655
+ self.freqs_cis = buf
656
+ return self.freqs_cis
657
+
658
+
659
+ def _init_weights(self, module):
660
+ super()._init_weights(module)
661
+ # HC head initialization
662
+ if module is self:
663
+ nn.init.normal_(self.hc_head_fn, std=0.01)
664
+ nn.init.zeros_(self.hc_head_base)
665
+ nn.init.ones_(self.hc_head_scale)
666
+
667
+ def hc_head(self, x):
668
+ """Contract hc_mult copies to 1 for final output.
669
+
670
+ x: [B, S, hc_mult, D] -> [B, S, D]
671
+ """
672
+ shape = x.size()
673
+ dtype = x.dtype
674
+ x_flat = x.flatten(2).float() # [B, S, hc_mult*D]
675
+
676
+ rsqrt = torch.rsqrt(x_flat.pow(2).mean(-1, keepdim=True) + self.config.rms_norm_eps)
677
+ mixes = F.linear(x_flat, self.hc_head_fn.float()) * rsqrt # [B, S, hc_mult]
678
+
679
+ pre = torch.sigmoid(mixes * self.hc_head_scale.float() + self.hc_head_base.float()) + self.config.hc_eps
680
+ y = (pre.unsqueeze(-1) * x.float()).sum(dim=2)
681
+ return y.to(dtype)
682
+
683
+ def forward(
684
+ self,
685
+ input_ids: Optional[torch.LongTensor] = None,
686
+ attention_mask: Optional[torch.Tensor] = None,
687
+ position_ids: Optional[torch.LongTensor] = None,
688
+ past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
689
+ inputs_embeds: Optional[torch.FloatTensor] = None,
690
+ use_cache: Optional[bool] = None,
691
+ output_hidden_states: Optional[bool] = None,
692
+ return_dict: Optional[bool] = None,
693
+ ) -> BaseModelOutputWithPast:
694
+ return_dict = return_dict if return_dict is not None else True # LILO: return_dict safe default (transformers 5.x)
695
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
696
+
697
+ if input_ids is not None and inputs_embeds is not None:
698
+ raise ValueError("Cannot specify both input_ids and inputs_embeds")
699
+
700
+ if inputs_embeds is None:
701
+ inputs_embeds = self.embed_tokens(input_ids)
702
+
703
+ bsz, seqlen = inputs_embeds.shape[:2]
704
+
705
+ # LILO: honor KV cache (original hardcoded use_cache=False which
706
+ # recomputed the full sequence on every decode step)
707
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
708
+ if use_cache and past_key_values is not None and len(past_key_values) > 0 and past_key_values[0] is not None:
709
+ past_len = int(past_key_values[0][0].shape[2])
710
+ else:
711
+ past_key_values = None
712
+ past_len = 0
713
+
714
+ if position_ids is None:
715
+ position_ids = torch.arange(past_len, past_len + seqlen, device=inputs_embeds.device).unsqueeze(0)
716
+
717
+ # Get freqs for RoPE
718
+ # freqs_cis is [2, max_seq, D//2], index by position
719
+ pos = position_ids.squeeze(0)
720
+ freqs_cis = self.get_freqs_cis()[:, pos].to(inputs_embeds.device) # [2, seqlen, D//2]
721
+
722
+ # The attention layer builds its own causal mask from absolute
723
+ # positions (KV-cache aware), so we do not pass a mask down.
724
+ causal_mask = None
725
+
726
+
727
+ # Expand to hc_mult copies
728
+ hidden_states = inputs_embeds.unsqueeze(2).expand(-1, -1, self.config.hc_mult, -1)
729
+ hidden_states = hidden_states.contiguous()
730
+
731
+ new_past_key_values = [] if use_cache else None
732
+
733
+ for i, layer in enumerate(self.layers):
734
+ past_kv = past_key_values[i] if past_key_values is not None and i < len(past_key_values) else None
735
+
736
+ if self.gradient_checkpointing and self.training:
737
+ hidden_states, new_cache = torch.utils.checkpoint.checkpoint(
738
+ layer, hidden_states, causal_mask, position_ids, freqs_cis, past_kv, use_cache,
739
+ use_reentrant=False,
740
+ )
741
+ else:
742
+ hidden_states, new_cache = layer(
743
+ hidden_states, attention_mask=causal_mask, position_ids=position_ids,
744
+ freqs_cis=freqs_cis, past_key_value=past_kv, use_cache=use_cache,
745
+ )
746
+
747
+ if use_cache:
748
+ new_past_key_values.append(new_cache)
749
+
750
+ # Contract HC copies -> single hidden state
751
+ hidden_states = self.hc_head(hidden_states)
752
+ hidden_states = self.norm(hidden_states)
753
+
754
+ if not return_dict:
755
+ return (hidden_states, new_past_key_values)
756
+
757
+ return BaseModelOutputWithPast(
758
+ last_hidden_state=hidden_states,
759
+ past_key_values=new_past_key_values,
760
+ )
761
+
762
+
763
+ class DeepseekV4ForCausalLM(DeepseekV4PreTrainedModel, GenerationMixin):
764
+ _tied_weights_keys = {"lm_head.weight": "model.embed_tokens.weight"}
765
+
766
+ def __init__(self, config: DeepseekV4Config):
767
+ super().__init__(config)
768
+ self.model = DeepseekV4Model(config)
769
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
770
+ self.post_init()
771
+
772
+ def get_input_embeddings(self):
773
+ return self.model.embed_tokens
774
+
775
+ def set_input_embeddings(self, value):
776
+ self.model.embed_tokens = value
777
+
778
+ def get_output_embeddings(self):
779
+ return self.lm_head
780
+
781
+ def set_output_embeddings(self, new_embeddings):
782
+ self.lm_head = new_embeddings
783
+
784
+ def forward(
785
+ self,
786
+ input_ids: Optional[torch.LongTensor] = None,
787
+ attention_mask: Optional[torch.Tensor] = None,
788
+ position_ids: Optional[torch.LongTensor] = None,
789
+ past_key_values: Optional[List[Tuple[torch.Tensor, torch.Tensor]]] = None,
790
+ inputs_embeds: Optional[torch.FloatTensor] = None,
791
+ labels: Optional[torch.LongTensor] = None,
792
+ use_cache: Optional[bool] = None,
793
+ output_hidden_states: Optional[bool] = None,
794
+ return_dict: Optional[bool] = None,
795
+ **kwargs,
796
+ ) -> CausalLMOutputWithPast:
797
+ return_dict = return_dict if return_dict is not None else True # LILO: return_dict safe default (transformers 5.x)
798
+
799
+ outputs = self.model(
800
+ input_ids=input_ids,
801
+ attention_mask=attention_mask,
802
+ position_ids=position_ids,
803
+ past_key_values=past_key_values,
804
+ inputs_embeds=inputs_embeds,
805
+ use_cache=use_cache,
806
+ output_hidden_states=output_hidden_states,
807
+ return_dict=False, # always tuple for compile compatibility
808
+ )
809
+
810
+ hidden_states = outputs[0]
811
+ logits = self.lm_head(hidden_states)
812
+
813
+ loss = None
814
+ if labels is not None:
815
+ shift_logits = logits[..., :-1, :].contiguous()
816
+ shift_labels = labels[..., 1:].contiguous()
817
+ loss = F.cross_entropy(
818
+ shift_logits.view(-1, self.config.vocab_size),
819
+ shift_labels.view(-1),
820
+ ignore_index=-100,
821
+ )
822
+
823
+ if not return_dict:
824
+ output = (logits,) + outputs[1:]
825
+ return (loss,) + output if loss is not None else output
826
+
827
+ past_kv = outputs[1] if len(outputs) > 1 else None
828
+ return CausalLMOutputWithPast(
829
+ loss=loss,
830
+ logits=logits,
831
+ past_key_values=past_kv,
832
+ )
833
+
834
+ def prepare_inputs_for_generation(self, input_ids, past_key_values=None, **kwargs):
835
+ if past_key_values is not None:
836
+ input_ids = input_ids[:, -1:]
837
+
838
+ return {
839
+ "input_ids": input_ids,
840
+ "past_key_values": past_key_values,
841
+ "use_cache": True,
842
+ }
run_cpu.sh ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+ # LILO CPU runner — memory-safe: never touches /tmp (tmpfs), bounded threads.
3
+ set -euo pipefail
4
+ cd "$(dirname "$0")"
5
+
6
+ export TMPDIR="$PWD/.tmp" # MUST be disk-backed, not tmpfs
7
+ export HF_HOME="${HF_HOME:-$PWD/.hf}"
8
+ export OMP_NUM_THREADS="${OMP_NUM_THREADS:-2}"
9
+ export MALLOC_ARENA_MAX=1 # keep glibc from reserving RAM per thread
10
+ export TOKENIZERS_PARALLELISM=false
11
+
12
+ mkdir -p "$TMPDIR" "$HF_HOME"
13
+
14
+ PY="${PYTHON:-python3}"
15
+ if [ -x "$PWD/.venv/bin/python" ]; then
16
+ PY="$PWD/.venv/bin/python"
17
+ fi
18
+
19
+ exec "$PY" chat_lilo.py "$@"
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "backend": "tokenizers",
3
+ "bos_token": "<|begin▁of▁sentence|>",
4
+ "eos_token": "<|end▁of▁sentence|>",
5
+ "is_local": true,
6
+ "local_files_only": false,
7
+ "model_max_length": 1000000000000000019884624838656,
8
+ "pad_token": "<|end▁of▁sentence|>",
9
+ "tokenizer_class": "TokenizersBackend"
10
+ }