Anoy123423123 commited on
Commit
5e27996
·
verified ·
1 Parent(s): 97d0385

Anonymous code release: MSA inference and evaluation

Browse files
QUICK_START.md ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Project Structure
2
+
3
+ ```
4
+ MSA/
5
+ ├── scripts/
6
+ │ ├── run_benchmarks.sh # Run inference on benchmarks
7
+ │ ├── calculate_llm_score.sh # LLM-based answer evaluation
8
+ │ └── resave_model.sh # Convert base model to MSA format
9
+ └── src/
10
+ ├── msa/ # Core MSA implementation
11
+ │ ├── configuration_msa.py # MSA model configuration
12
+ │ ├── memory_sparse_attention.py # MemorySparseAttention layer
13
+ │ ├── model.py # MSAForCausalLM / MSAModel
14
+ │ └── generate.py # Generation logic
15
+ ├── config/
16
+ │ └── memory_config.py # GenerateConfig, ModelConfig, MemoryConfig
17
+ ├── evaluation/
18
+ │ └── llm_judge.py # LLM-based evaluation metrics
19
+ ├── app/
20
+ │ └── benchmark.py # Benchmark runner
21
+ ├── utils/ # GPU workers, caching, templates, etc.
22
+ ├── msa_service.py # Multi-GPU inference engine (MSAEngine)
23
+ ├── prefill.py # Stage 1 prefill worker
24
+ ├── benchmarks.py # Benchmark registry & specs
25
+ └── types.py # Core type definitions
26
+ ```
27
+
28
+ ## Installation
29
+
30
+ **1. Create conda environment**
31
+
32
+ ```bash
33
+ conda create -n msa python=3.12 -y
34
+ conda activate msa
35
+ ```
36
+
37
+ **2. Install dependencies**
38
+
39
+ ```bash
40
+ pip install -r requirements.txt
41
+ ```
42
+
43
+ <details>
44
+ <summary>requirements.txt</summary>
45
+
46
+ ```
47
+ torch==2.6.0
48
+ torchvision==0.21.0
49
+ transformers==4.51.3 # exact version required
50
+ accelerate==1.0.1
51
+ liger_kernel==0.5.10
52
+ huggingface_hub==0.31.4 # must stay <1.0 for transformers 4.51.3
53
+ datasets==3.1.0
54
+ lmdb==1.6.2
55
+ tqdm==4.67.1
56
+ numpy==1.26.4
57
+ pillow==11.2.1
58
+ packaging==25.0
59
+ nvidia-ml-py==12.575.51 # provides the `pynvml` module
60
+ openai==1.79.0
61
+ ```
62
+
63
+ </details>
64
+
65
+ **3. Install Flash Attention**
66
+
67
+ Option A — build from source:
68
+
69
+ ```bash
70
+ pip install flash-attn==2.7.4.post1 --no-build-isolation
71
+ ```
72
+
73
+ Option B — install prebuilt wheel (CUDA 12, Python 3.12):
74
+
75
+ ```bash
76
+ wget -P /tmp https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp312-cp312-linux_x86_64.whl
77
+ pip install /tmp/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp312-cp312-linux_x86_64.whl
78
+ rm /tmp/flash_attn-2.7.4.post1+cu12torch2.6cxx11abiFALSE-cp312-cp312-linux_x86_64.whl
79
+ ```
80
+
81
+ ## Download
82
+
83
+ **1. Download model**
84
+
85
+ ```bash
86
+ mkdir ckpt
87
+ pip install -U huggingface_hub==0.31.4
88
+ export HF_ENDPOINT=https://hf-mirror.com
89
+ huggingface-cli download --resume-download Anoy123423123/MSA-4B --local-dir ckpt/MSA-4B
90
+ ```
91
+
92
+ **2. Download benchmarks**
93
+
94
+ Benchmark data is hosted on [Anoy123423123/MSA-RAG-BENCHMARKS](https://huggingface.co/datasets/Anoy123423123/MSA-RAG-BENCHMARKS) and will be automatically downloaded to `data/` on first run, based on the benchmarks specified in `scripts/run_benchmarks.sh`. No manual download is needed.
95
+
96
+ ## Quick Start
97
+
98
+ **1. Run inference on benchmarks**
99
+
100
+ ```bash
101
+ bash scripts/run_benchmarks.sh eval_benchmark
102
+ ```
103
+
104
+ **2. Compute LLM-based scores**
105
+
106
+ ```bash
107
+ bash scripts/calculate_llm_score.sh eval_benchmark
108
+ ```
109
+
110
+ ## Supported Benchmarks
111
+
112
+ | Category | Benchmark |
113
+ |---|---|
114
+ | Multi-hop QA | `2wikimultihopqa`, `hotpotqa`, `musique` |
115
+ | Single-hop QA | `nature_questions`, `triviaqa_06M`, `triviaqa_10M`, `msmarco_v1`, `dureader`, `ms_100M`, `hipporag_narrative`, `hipporag_popqa` |
README.md ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ tags:
6
+ - msa
7
+ ---
8
+ # MSA: Memory Sparse Attention
9
+
10
+ *A scalable, end-to-end trainable latent-memory framework for 100M-token contexts*
11
+
12
+ **Anonymous code release for peer review.**
13
+
14
+ | | |
15
+ |---|---|
16
+ | Code (this repo) | `Anoy123423123/MSA-Code` |
17
+ | Model weights | [`Anoy123423123/MSA-4B`](https://huggingface.co/Anoy123423123/MSA-4B) |
18
+ | Benchmark data | [`Anoy123423123/MSA-RAG-BENCHMARKS`](https://huggingface.co/datasets/Anoy123423123/MSA-RAG-BENCHMARKS) |
19
+
20
+ Jump to [**Quick Start**](#-quick-start) to reproduce the reported numbers.
21
+
22
+ ## 📝 Abstract
23
+
24
+ Long-term memory is essential for general intelligence, yet the **full attention** bottleneck constrains most LLMs' **effective context length** to **128K–1M**. Existing attempts — hybrid linear attention, fixed-size state memory (e.g., RNNs), and external storage like **RAG/agents** — either suffer rapid precision decay and latency growth at extreme scales, lack end-to-end differentiability or dynamic memory maintenance, or require complex pipelines. We present **Memory Sparse Attention (MSA)**: an **end-to-end trainable, scalable sparse latent-state memory** framework. Core ideas include:
25
+
26
+ - **Scalable sparse attention** + **document-wise RoPE** (parallel/global) achieving **near-linear complexity** in both training and inference;
27
+ - **KV cache compression** with a **Memory Parallel** inference engine to deliver **100M token** throughput on **2×A800** GPUs;
28
+ - **Memory Interleave** for multi-round, multi-hop reasoning across scattered memory segments.
29
+
30
+ On long-context QA and NIAH (Needle-in-a-Haystack) benchmarks, **MSA** surpasses same-backbone RAG, best-of-breed RAG stacks, and leading long-context models. Across an unprecedented **16K→100M token** range, MSA shows **< 9%** degradation, suggesting a practical path to **decouple memory capacity from reasoning**.
31
+
32
+ > **Scaling from 16K→100M tokens**: MSA fuses top-k selection with sparse attention to remain end-to-end differentiable while allowing document decoupling at inference. On MS MARCO, MSA sustains **<9%** degradation and exhibits strong extrapolation.
33
+
34
+ ## ✨ Key Contributions
35
+
36
+ - **Memory-Sparse Attention (MSA)**: an **end-to-end trainable**, **scalable sparse attention** layer with **document-wise RoPE**, realizing **O(L)** complexity and **<9%** degradation from **16K→100M tokens**.
37
+ - **KV cache compression + Memory Parallel**: tiered storage (GPU-resident routing keys, CPU content K/V), distributed scoring, and on-demand transfers to enable **100M-token** inference on **2×A800**.
38
+ - **Memory Interleave**: adaptive alternating "generative retrieval → context expansion → generation," significantly boosting **multi-hop** reasoning across documents.
39
+ - **Comprehensive evaluation**: MSA outperforms same-backbone RAG, best-of-breed RAG pipelines, and top long-context models on long-context QA and NIAH, showing superior **stability** and **accuracy** at scale.
40
+
41
+ ---
42
+
43
+ ## 🧩 Overall Design
44
+
45
+ ### Architecture
46
+
47
+ MSA integrates **retrieval and generation** into a single differentiable loop. Document latent states (**K/V/Kᵣ**) are **chunk-mean pooled** for compression. A **router projector** computes relevance via cosine similarity (mean-pooled over heads, then token-wise max), selects **Top‑k documents**, then concatenates their **compressed K/V** with the query's local **K/V** for autoregressive decoding. Routing applies only to **upper layers**; lower layers keep **independent document processing** for hierarchical alignment.
48
+
49
+ - **Parallel (document-wise) RoPE**: Each document resets positions from 0, preventing position drift between **train-short** and **infer-long**, enabling 64k training to extrapolate to 100M.
50
+ - **Global RoPE (active context)**: The query's starting index is **offset by k** (Top‑k retrieved blocks), preserving causal ordering: *background → query → generation*.
51
+
52
+ Implemented in [`src/msa/memory_sparse_attention.py`](./src/msa/memory_sparse_attention.py) and [`src/msa/model.py`](./src/msa/model.py).
53
+
54
+ ---
55
+
56
+ ### Inference Pipeline
57
+
58
+ MSA uses a **three-stage** pipeline:
59
+
60
+ 1. **Global Memory Encoding (offline)**: forward over the corpus to cache chunk-pooled **(K̄, V̄, K̄ᵣ)**.
61
+ 2. **Online Routing & Context Assembly**: project query to **Qᵣ**, match with **K̄ᵣ** to pick **Top‑k**, then load only the selected **K̄/V̄** and concatenate with local context.
62
+ 3. **Sparse Generation**: autoregress over the **sparse context**.
63
+
64
+ **Memory Parallel** shards **K̄ᵣ** across GPUs (query broadcast → local scoring → global reduce). Content **K̄/V̄** stays in host DRAM and is **asynchronously fetched** when selected—balancing **VRAM** and **throughput** for **100M-token** deployment.
65
+
66
+ Implemented in [`src/msa_service.py`](./src/msa_service.py) (`MSAEngine`, memory-parallel bucketing) and [`src/prefill.py`](./src/prefill.py) (stage-1 encoding).
67
+
68
+ ---
69
+
70
+ ## 🚀 Results
71
+
72
+ > **Setup**
73
+ > **QA**: 9 datasets (MS MARCO v1, NQ, DuReader, TriviaQA(10M), NarrativeQA, PopQA, 2WikiMultiHopQA, HotpotQA, MuSiQue), memory banks **277K→10M tokens**, metric: **LLM judge (0–5)**.
74
+ > **NIAH (RULER)**: 8 subtasks, **32K→1M tokens**, report average accuracy.
75
+ > **Backbone**: Qwen3‑4B‑Instruct‑2507. Compare to same-backbone RAG and best-of-breed RAG stacks (KaLMv2 + large generators, optional reranker).
76
+
77
+ ### Table 1: MSA vs same-backbone RAG (Qwen3‑4B)
78
+
79
+ **Summary**: Average **3.760**, improving over standard RAG (**+16.0%**), RAG+rerank (**+11.5%**), and HippoRAG2 (**+14.8%**) using their best@k; MSA leads on all but NarrativeQA within the same-backbone group.
80
+
81
+ | Dataset | Tokens | Qwen3-4B R@1 | R@5 | R@10 | Qwen3-4B (RR) R@1 | R@5 | R@10 | HippoRAG2 R@1 | R@5 | R@10 | MSA (adaptive) |
82
+ |---------|--------|--------------|------|-------|----------------------|------|-------|------------------|------|--------|----------------|
83
+ | MS MARCO v1 | 7.34M | 2.893 | 3.011 | 3.005 | 2.934 | <u>3.032</u> | 3.017 | 2.676 | 3.005 | 3.019 | **4.141** |
84
+ | Natural Questions | 1.47M | 3.452 | 3.374 | 3.297 | <u>3.494</u> | 3.408 | 3.385 | 3.338 | 3.389 | 3.374 | **3.545** |
85
+ | DuReader | 277K | 3.726 | 3.579 | 3.594 | <u>3.848</u> | 3.618 | 3.607 | 2.941 | 3.485 | 3.415 | **4.155** |
86
+ | TriviaQA (10M) | 10M | 4.133 | 4.414 | 4.273 | 4.313 | 4.375 | 4.391 | 4.188 | <u>4.430</u> | 4.367 | **4.621** |
87
+ | NarrativeQA | 538K | 1.611 | 2.567 | 2.860 | **3.638** | 3.492 | <u>3.536</u> | 1.959 | 2.628 | 2.655 | 3.395 |
88
+ | PopQA | 1.18M | 2.959 | 3.273 | 3.299 | <u>3.315</u> | 3.264 | 3.266 | 3.111 | 3.249 | 3.249 | **3.433** |
89
+ | 2WikiMultiHopQA | 722K | 1.065 | 3.055 | 3.136 | 1.187 | 3.057 | 3.159 | 1.045 | 3.180 | <u>3.330</u> | **4.280** |
90
+ | HotpotQA | 1.35M | 2.252 | 3.582 | 3.787 | 2.642 | 3.990 | <u>4.022</u> | 3.230 | 3.770 | 3.970 | **4.061** |
91
+ | MuSiQue | 1.41M | 0.936 | 1.752 | 1.928 | 1.144 | 1.960 | 1.965 | 1.020 | 1.907 | <u>2.095</u> | **2.211** |
92
+ | **Average** | — | 2.559 | 3.179 | 3.242 | 2.946 | 3.355 | <u>3.372</u> | 2.612 | 3.227 | 3.275 | **3.760** |
93
+
94
+ *Table 1: Same-backbone RAG vs MSA (@1/@5/@10 vs MSA @adaptive)*
95
+
96
+ ---
97
+
98
+ ### Table 2: MSA vs best-of-breed RAG (large backbones)
99
+
100
+ **Summary**: Against **KaLMv2+Qwen3‑235B** and **KaLMv2+Llama‑3.3‑70B** (w/ and w/o reranking), MSA achieves the best score on **4/9** datasets and an average **3.760**, with relative gains of **+7.2%**, **+5.0%**, **+10.7%**, and **+5.4%** over the strongest configurations respectively. Gaps on a few datasets (e.g., MuSiQue) are largely attributable to parameter-count and intrinsic reasoning capacity.
101
+
102
+ | Dataset | KaLMv2 + Qwen3‑235B R@1 | R@5 | R@10 | Qwen3‑235B (RR) R@1 | R@5 | R@10 | KaLMv2 + Llama‑3.3 R@1 | R@5 | R@10 | Llama‑3.3 (RR) R@1 | R@5 | R@10 | MSA (adaptive) |
103
+ |---------|---------------------------|------|-------|--------------------------|------|-------|------------------------------|------|--------|-------------------------|------|--------|----------------|
104
+ | MS MARCO v1 | 2.846 | <u>3.028</u> | 3.027 | 2.886 | 3.020 | 2.995 | 2.649 | 2.904 | 2.919 | 2.881 | 2.955 | 2.952 | **4.141** |
105
+ | Natural Questions | <u>3.711</u> | 3.670 | 3.694 | 3.621 | 3.610 | 3.645 | 3.675 | 3.674 | 3.662 | **3.756** | 3.665 | 3.647 | 3.545 |
106
+ | DuReader | 4.044 | 3.991 | 3.978 | 3.973 | 3.932 | 3.891 | <u>4.051</u> | 3.846 | 3.742 | 3.967 | 3.776 | 3.780 | **4.155** |
107
+ | TriviaQA (10M) | 4.367 | 4.656 | 4.578 | 4.492 | 4.320 | 4.555 | 4.273 | **4.740** | <u>4.719</u> | 4.547 | 4.703 | 4.695 | 4.621 |
108
+ | NarrativeQA | 1.413 | 2.130 | 2.427 | 3.212 | **3.427** | 3.375 | 1.290 | 2.123 | 2.382 | 3.150 | 3.263 | 3.317 | <u>3.395</u> |
109
+ | PopQA | 2.810 | 3.347 | <u>3.396</u> | 3.268 | 3.380 | 3.376 | 2.787 | 3.298 | 3.305 | 3.337 | 3.384 | 3.362 | **3.433** |
110
+ | 2WikiMultiHopQA | 2.646 | 3.579 | 3.582 | 1.855 | 3.381 | <u>3.583</u> | 1.339 | 3.263 | 3.445 | 1.651 | 3.332 | 3.541 | **4.280** |
111
+ | HotpotQA | 3.497 | 4.090 | **4.225** | 3.341 | 4.141 | 4.194 | 3.070 | 3.896 | 4.127 | 3.428 | 4.145 | <u>4.203</u> | 4.061 |
112
+ | MuSiQue | 1.988 | 2.462 | **2.647** | 1.801 | 2.522 | 2.605 | 1.704 | 2.317 | 2.258 | 1.895 | 2.462 | <u>2.614</u> | 2.211 |
113
+ | **Average** | 3.036 | 3.439 | 3.506 | 3.161 | 3.526 | <u>3.580</u> | 2.760 | 3.340 | 3.396 | 3.179 | 3.521 | 3.568 | **3.760** |
114
+
115
+ *Table 2: SOTA RAG stacks (strong retriever + large generator + optional reranker) vs MSA*
116
+
117
+ ---
118
+
119
+ ### RULER NIAH stability (32K→1M)
120
+
121
+ **Summary**: MSA maintains **94.84%** at **1M tokens**. The unmodified backbone collapses beyond **128K** (down to **24.69% @1M**). Hybrid linear-attention long-context models degrade noticeably at **≥128K/256K**. External-memory agents (e.g., RL‑MemoryAgent‑14B) remain stable but are weaker in **absolute accuracy** and show steeper decay than MSA.
122
+
123
+ ---
124
+
125
+ ## Implementation Notes
126
+
127
+ - **Training**: 158.95B-token continuous pretraining with **auxiliary routing loss**, followed by two-stage SFT (**8k→64k** curriculum).
128
+ - **Ablations** (paper Table 4): curriculum extension, Memory Interleave, continuous pretraining, and injecting original text all contribute substantially; removing them causes **5%–37%** drops depending on task.
129
+ - **Training cost**: MSA's near-linear complexity shows up directly in wall-clock time — full attention grows quadratically with context length while MSA stays close to linear.
130
+
131
+ ---
132
+
133
+ ## 🚀 Quick Start
134
+
135
+ > This section is self-contained. [**QUICK_START.md**](./QUICK_START.md) additionally documents a
136
+ > prebuilt `flash-attn` wheel, which avoids a slow source build.
137
+
138
+ ### Requirements
139
+
140
+ | | |
141
+ |---|---|
142
+ | OS | Linux, x86-64 |
143
+ | Python | 3.12 (tested on 3.12.9) |
144
+ | CUDA | 12.x |
145
+ | GPUs | **8× 80GB** for the default benchmark suite; **2× 80GB (A800)** is enough for the `ms_100M` 100M-token setting via Memory Parallel |
146
+ | Disk | ~9 GB for weights + ~0.6 GB for benchmark data |
147
+
148
+ `transformers` **must** be exactly `4.51.3` — the attention implementation targets that API and will not
149
+ load on 4.52+ or on 5.x. The other pins in `requirements.txt` are the versions everything was tested with.
150
+
151
+ ### 1. Get the code
152
+
153
+ ```bash
154
+ pip install -U "huggingface_hub==0.31.4"
155
+ # If you are behind a mirror, uncomment the next line:
156
+ # export HF_ENDPOINT=https://hf-mirror.com
157
+
158
+ huggingface-cli download Anoy123423123/MSA-Code --repo-type=model --local-dir MSA
159
+ cd MSA
160
+ ```
161
+
162
+ > `git clone https://huggingface.co/Anoy123423123/MSA-Code` works too.
163
+
164
+ ### 2. Install dependencies
165
+
166
+ ```bash
167
+ conda create -n msa python=3.12 -y
168
+ conda activate msa
169
+ pip install -r requirements.txt
170
+ ```
171
+
172
+ `flash-attn` is installed separately because it needs `--no-build-isolation`:
173
+
174
+ ```bash
175
+ pip install flash-attn==2.7.4.post1 --no-build-isolation
176
+ ```
177
+
178
+ Verify:
179
+
180
+ ```bash
181
+ python -c "import torch, transformers, flash_attn, pynvml; print(torch.__version__, transformers.__version__, flash_attn.__version__)"
182
+ # expected: 2.6.0 4.51.3 2.7.4.post1
183
+ ```
184
+
185
+ ### 3. Download the model weights
186
+
187
+ ```bash
188
+ huggingface-cli download --resume-download Anoy123423123/MSA-4B --local-dir ckpt/MSA-4B
189
+ ```
190
+
191
+ This is the path `scripts/run_benchmarks.sh` expects (`model_path=ckpt/MSA-4B`). ~9 GB.
192
+
193
+ ### 4. Benchmark data
194
+
195
+ Nothing to do — benchmark files are fetched on first use from
196
+ [`Anoy123423123/MSA-RAG-BENCHMARKS`](https://huggingface.co/datasets/Anoy123423123/MSA-RAG-BENCHMARKS)
197
+ into `./data/<benchmark>/`. Each benchmark is two pickle files: `mdata_*.pkl` (memory corpus) and
198
+ `qdata_*.pkl` (queries). ~0.6 GB for everything; `ms_100M` alone is 440 MB.
199
+
200
+ ### 5. Run inference
201
+
202
+ ```bash
203
+ bash scripts/run_benchmarks.sh eval_benchmark
204
+ ```
205
+
206
+ Results land in `src/evaluation/outputs/eval_benchmark/` — one `.log` and one `.json` per benchmark —
207
+ and retrieval precision metrics are printed as each benchmark finishes.
208
+
209
+ Check the top of `scripts/run_benchmarks.sh` first:
210
+
211
+ ```bash
212
+ export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 # defaults to 8 GPUs
213
+ model_path=ckpt/MSA-4B
214
+ ```
215
+
216
+ The `benchmarks` array holds `name:batch_size` entries. **Batch size must be scaled together with the
217
+ GPU count**: the memory corpus is partitioned into one bucket per visible GPU, so the defaults below
218
+ assume 8 GPUs.
219
+
220
+ | Category | Benchmark | Default batch size |
221
+ |---|---|---|
222
+ | Multi-hop QA | `2wikimultihopqa`, `hotpotqa`, `musique` | 16 |
223
+ | Single-hop QA | `nature_questions`, `triviaqa_06M` | 16 |
224
+ | Single-hop QA (large corpus) | `triviaqa_10M` | 2 |
225
+ | HippoRAG | `hipporag_narrative`, `hipporag_popqa` | 16 |
226
+ | Passage retrieval / multilingual | `msmarco_v1`, `dureader` | 16 |
227
+ | Length scaling (100M tokens) | `ms_100M` | 16 |
228
+
229
+ `ms_100M` and `triviaqa_06M` are commented out by default — uncomment them to reproduce the
230
+ length-scaling results. `ms_100M` is the 100M-token setting and is by far the heaviest run.
231
+
232
+ ### 6. Score the answers
233
+
234
+ Answer quality is graded by an LLM judge served through OpenRouter:
235
+
236
+ ```bash
237
+ export OPENROUTER_API_KEY=<your key> # or edit scripts/calculate_llm_score.sh
238
+ bash scripts/calculate_llm_score.sh eval_benchmark
239
+ ```
240
+
241
+ The argument must match the log directory name from step 5.
242
+
243
+ ---
244
+
245
+ ## Project Structure
246
+
247
+ ```
248
+ ├── scripts/
249
+ │ ├── run_benchmarks.sh # Run inference on benchmarks
250
+ │ ├── calculate_llm_score.sh # LLM-based answer evaluation
251
+ │ └── resave_model.sh # Convert base model to MSA format
252
+ └── src/
253
+ ├── msa/ # Core MSA implementation
254
+ │ ├── configuration_msa.py # MSA model configuration
255
+ │ ├── memory_sparse_attention.py # MemorySparseAttention layer
256
+ │ ├── model.py # MSAForCausalLM / MSAModel
257
+ │ └── generate.py # Generation logic
258
+ ├── config/
259
+ │ └── memory_config.py # GenerateConfig, ModelConfig, MemoryConfig
260
+ ├── evaluation/
261
+ │ └── llm_judge.py # LLM-based evaluation metrics
262
+ ├── app/
263
+ │ └── benchmark.py # Benchmark runner
264
+ ├── utils/ # GPU workers, caching, templates, etc.
265
+ ├── msa_service.py # Multi-GPU inference engine (MSAEngine)
266
+ ���── prefill.py # Stage 1 prefill worker
267
+ ├── benchmarks.py # Benchmark registry & specs
268
+ └── types.py # Core type definitions
269
+ ```
270
+
271
+ ---
272
+
273
+ ## Troubleshooting
274
+
275
+ | Symptom | Cause / fix |
276
+ |---|---|
277
+ | `KeyError` / `AttributeError` while loading the model | Wrong `transformers` version. It must be exactly `4.51.3`. |
278
+ | `import pynvml` fails or NVML calls are missing | The `pynvml` module here comes from `nvidia-ml-py`. If the separate, deprecated `pynvml` distribution is also installed it shadows the module: `pip uninstall -y pynvml && pip install nvidia-ml-py==12.575.51`. |
279
+ | CUDA OOM | Lower the per-benchmark `batch_size` in `scripts/run_benchmarks.sh`, or reduce the GPU count together with the batch size. |
280
+ | `flash_attn` import error | The wheel must match your torch / Python / CUDA combination. Build from source with `--no-build-isolation`. |
281
+ | Benchmark data download stalls | Set `export HF_ENDPOINT=https://hf-mirror.com` and retry; partial files resume. |
282
+
283
+ ---
284
+
285
+ ## Citation
286
+
287
+ Citation details are withheld during anonymous peer review.
288
+
289
+ ## License
290
+
291
+ MIT.
requirements.txt ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Tested with Python 3.12.9 on CUDA 12.x.
2
+ # NOTE: flash-attn is intentionally NOT listed here, it needs --no-build-isolation.
3
+ # Install it separately, see step 3 of QUICK_START.md.
4
+
5
+ # --- Core model stack (versions are strict: MSA targets the transformers 4.51 API) ---
6
+ torch==2.6.0
7
+ torchvision==0.21.0
8
+ transformers==4.51.3
9
+ accelerate==1.0.1
10
+ liger_kernel==0.5.10
11
+
12
+ # --- Hub / data IO ---
13
+ # huggingface_hub must stay <1.0 to remain compatible with transformers 4.51.3.
14
+ huggingface_hub==0.31.4
15
+ datasets==3.1.0
16
+ lmdb==1.6.2
17
+ tqdm==4.67.1
18
+
19
+ # --- Numerics ---
20
+ numpy==1.26.4
21
+ pillow==11.2.1
22
+ packaging==25.0
23
+
24
+ # --- GPU monitoring: provides the `pynvml` module.
25
+ # Do NOT replace with the deprecated `pynvml` package, its import path differs.
26
+ nvidia-ml-py==12.575.51
27
+
28
+ # --- LLM-based answer scoring (scripts/calculate_llm_score.sh) ---
29
+ openai==1.79.0
scripts/calculate_llm_score.sh ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ export OPENROUTER_API_KEY="your_openrouter_key"
4
+ exp_name=$1
5
+ python src/evaluation/llm_judge.py src/evaluation/outputs/${exp_name}
scripts/resave_model.sh ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 模型超参数
2
+ export POOLING_KERNEL_SIZE=64
3
+ export TOP_K_DOCS=16
4
+
5
+ # trick
6
+ export AUX_LOSS="false"
7
+
8
+ export REWRITE_POSITION="true"
9
+ export ROUTER_LAYER_IDX="18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35"
10
+ # export ROUTER_LAYER_IDX="20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39"
11
+
12
+ # special tokens
13
+ export ADD_MEM_TOKENS="false"
14
+ export NUM_PREFIX_TOKENS=64
15
+ export NUM_TAIL_TOKENS=64
16
+ export FIRST_ADD_MEM_TOKENS="false"
17
+ export WARM_UP_MEM_TOKENS="false"
18
+
19
+
20
+ # loss weight
21
+ export LMLOSS_WEIGHT=1.0
22
+ export REC_LOSS_WEIGHT=0.0
23
+ export AUX_LOSS_WEIGHT=0.1
24
+ export ANS_LOSS_WEIGHT=1.0
25
+ export AUX_LOSS_METHOD="INFONCE_DECOUPLE" # INFONCE, INFONCE_FOCAL, BCE, INFONCE_DECOUPLE, INFONCE_DECOUPLE_FOCAL
26
+ export INFONCE_LOSS_TEMP=0.1 # 如果使用infonce的话
27
+
28
+ # 结构搜索
29
+ export DECOUPLE_ROUTER="true"
30
+ export HEAD_REDUCE_METHOD="mean"
31
+ export QUERY_REDUCE_METHOD="max"
32
+ export CHUNK_REDUCE_METHOD="max"
33
+ export DECOUPLE_POOLING_MODE="mean"
34
+
35
+ ORIGIN_MODEL_PATH=$1
36
+ SAVE_MODEL_PATH=$2
37
+
38
+ python src/utils/resave_model.py $ORIGIN_MODEL_PATH $SAVE_MODEL_PATH
scripts/run_benchmarks.sh ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ export MASTER_PORT=29509
3
+ export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
4
+ # export CUDA_VISIBLE_DEVICES=0,1
5
+
6
+ # model
7
+ model_path=ckpt/MSA-4B
8
+
9
+ # data
10
+ # bench_name:batch_size
11
+ # Adjust the batch size appropriately based on the number of GPUs.
12
+ benchmarks=(
13
+ # "ms_100M:16"
14
+ "hipporag_narrative:16"
15
+ "nature_questions:16"
16
+ "2wikimultihopqa:16"
17
+ "hotpotqa:16"
18
+ "musique:16"
19
+ "hipporag_popqa:16"
20
+ # "triviaqa_06M:16"
21
+ "triviaqa_10M:2"
22
+ "dureader:16"
23
+ "msmarco_v1:16"
24
+ )
25
+
26
+ top_p=0.9
27
+ temperature=0.0
28
+ max_length=2048
29
+ template=QWEN3_INSTRUCT_TEMPLATE
30
+
31
+ # Create log directory (use first argument as custom name, otherwise use timestamp)
32
+ log_name="${1:-eval_logs_$(date +%Y%m%d_%H%M%S)}"
33
+ log_dir="./src/evaluation/outputs/${log_name}"
34
+ if [ -d "$log_dir" ]; then
35
+ echo "Error: log directory already exists: $log_dir"
36
+ exit 1
37
+ fi
38
+ mkdir -p "$log_dir"
39
+
40
+ # Statistics
41
+ total_benchmarks=${#benchmarks[@]}
42
+ current=0
43
+ success_count=0
44
+ fail_count=0
45
+ failed_benchmarks=()
46
+
47
+ echo "=========================================="
48
+ echo "Start running all benchmark evaluations"
49
+ echo "Total: ${total_benchmarks} benchmarks"
50
+ echo "Log directory: $log_dir"
51
+ echo "=========================================="
52
+ echo ""
53
+
54
+ # Run each benchmark
55
+ for entry in "${benchmarks[@]}"; do
56
+ benchmark="${entry%%:*}"
57
+ batch_size="${entry##*:}"
58
+ current=$((current + 1))
59
+ echo "[$current/$total_benchmarks] Running: $benchmark (batch_size=$batch_size)"
60
+ echo "Start time: $(date '+%Y-%m-%d %H:%M:%S')"
61
+
62
+ # Create separate log file for each benchmark
63
+ log_file="$log_dir/${benchmark}.log"
64
+ json_file="$log_dir/${benchmark}.json"
65
+
66
+ # Run evaluation and record logs
67
+ python -u src/app/benchmark.py \
68
+ --benchmark "$benchmark" \
69
+ --model_path "$model_path" \
70
+ --top_p "$top_p" \
71
+ --temperature "$temperature" \
72
+ --max_length "$max_length" \
73
+ --template "$template" \
74
+ --output_file "$json_file" \
75
+ --max_batch_size "$batch_size" \
76
+ --max_chunk_per_block 16384 \
77
+ --block_size 2048 \
78
+ 2>&1 | tee $log_file
79
+
80
+ # Check exit status
81
+ exit_code=${PIPESTATUS[0]}
82
+
83
+ if [ $exit_code -eq 0 ]; then
84
+ echo "[$current/$total_benchmarks] $benchmark finished (success)"
85
+ # Print benchmark name and metrics
86
+ echo "========== $benchmark Results =========="
87
+ python -c "import json; d=json.load(open('$json_file')); [print(f' {k}: {v}') for k,v in d.get(list(d.keys())[0],{}).get('precision',{}).get('metrics',{}).items()]" 2>/dev/null || echo " (failed to parse metrics)"
88
+ echo "========================================"
89
+ success_count=$((success_count + 1))
90
+ else
91
+ echo "[$current/$total_benchmarks] $benchmark failed (exit code: $exit_code)"
92
+ fail_count=$((fail_count + 1))
93
+ failed_benchmarks+=("$benchmark")
94
+ fi
95
+
96
+ echo "End time: $(date '+%Y-%m-%d %H:%M:%S')"
97
+ echo "----------------------------------------"
98
+ echo ""
99
+ done
100
+
101
+ # Summary
102
+ echo "=========================================="
103
+ echo "All benchmark evaluations completed"
104
+ echo "=========================================="
105
+ echo "Total: $total_benchmarks"
106
+ echo "Success: $success_count"
107
+ echo "Failed: $fail_count"
108
+ echo ""
109
+
110
+ if [ $fail_count -gt 0 ]; then
111
+ echo "Failed benchmarks:"
112
+ for failed in "${failed_benchmarks[@]}"; do
113
+ echo " - $failed"
114
+ done
115
+ echo ""
116
+ fi
117
+
118
+ echo "All logs saved in: $log_dir"
119
+ echo "=========================================="
src/__init__.py ADDED
File without changes
src/app/__init__.py ADDED
File without changes
src/app/benchmark.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ from collections import OrderedDict
3
+ import pickle
4
+ import re
5
+ from typing import Dict, List
6
+ from tqdm import tqdm
7
+ import json
8
+ import torch
9
+ import numpy as np
10
+ import pathlib
11
+ import sys
12
+ import os
13
+ import random
14
+ import numpy as np
15
+ import multiprocessing as mp
16
+
17
+ project_path = pathlib.Path(__file__).parent.parent.parent
18
+ sys.path.append(str(project_path))
19
+
20
+ from src.benchmarks import BenchMarks
21
+
22
+
23
+ def set_seed(seed):
24
+ """
25
+ 固定所有随机种子以确保实验的可复现性。
26
+ """
27
+ random.seed(seed)
28
+ np.random.seed(seed)
29
+ torch.manual_seed(seed)
30
+ if torch.cuda.is_available():
31
+ torch.cuda.manual_seed(seed)
32
+ torch.cuda.manual_seed_all(seed) # 适用于多GPU环境
33
+ os.environ['PYTHONHASHSEED'] = str(seed)
34
+
35
+ # 在程序的开始部分调用此函数
36
+ seed_value = 42
37
+ set_seed(seed_value)
38
+
39
+ def parse_benchmark_file(args):
40
+ benchmark = BenchMarks(bench_name=args.benchmark)
41
+ path, mem_path = benchmark.get_bench_files()
42
+ args.query_file = path
43
+ args.memory_file = mem_path
44
+ print(" ==========> load query file from:", path)
45
+ if path.endswith(".json"):
46
+ with open(path, "r") as f:
47
+ raw_data = json.load(f)
48
+ ## 重组数据集
49
+ data = [{
50
+ 'question': d['question'],
51
+ 'labels': d['labels'],
52
+ 'answer': d['answer']
53
+ } for d in raw_data]
54
+ # 判断是否是一个路径
55
+ elif path.endswith("pkl"):
56
+ with open(path, "rb") as f:
57
+ query_metas = pickle.load(f)
58
+
59
+ data = [{
60
+ 'question': q_meta['query'],
61
+ 'labels': q_meta['reference_list'],
62
+ 'answer': q_meta['answer']
63
+ } for q_meta in query_metas]
64
+ else:
65
+ raise ValueError(f"Unsupported file format: {path}")
66
+ print(f"num sample: {len(data)}")
67
+ return data
68
+
69
+ def sort_requests(data_items: List[Dict]) -> List[dict]:
70
+ """
71
+ 创建动态批次:按照input_id长度排序,然后根据max_input_length分批
72
+ """
73
+ print("Tokenizing and sorting data for dynamic batching...")
74
+
75
+ # 计算每个item的input_id长度
76
+ items_with_length = []
77
+ for item in tqdm(data_items):
78
+ prompt = item["question"]
79
+ length = len(prompt)
80
+
81
+ items_with_length.append((item, length))
82
+
83
+ # 按长度排序
84
+ items_with_length.sort(key=lambda x: x[1])
85
+ return [item[0] for item in items_with_length]
86
+
87
+
88
+ def base_it(predict, label, at, score_func):
89
+ assert len(predict) == len(label)
90
+ scores = []
91
+ for pred, lbs in zip(predict, label):
92
+ pred = pred.tolist() if not isinstance(pred, list) else pred
93
+ best_score = 0.
94
+ if not isinstance(lbs, list):
95
+ lbs = [lbs]
96
+ for lb in lbs:
97
+ if isinstance(lb, list):
98
+ lb = lb[0]
99
+ rank = pred[:at].index(lb) + 1 if lb in pred[:at] else 0
100
+ cur_score = score_func(rank)
101
+ best_score = max(best_score, cur_score)
102
+ scores.append(best_score)
103
+ return scores
104
+
105
+
106
+ def eval_recall(predict, label, at=10):
107
+ scores = base_it(predict, label, at, lambda rank: int(rank != 0))
108
+ return {f'R@{at}': sum(scores) / len(scores)}
109
+
110
+
111
+ def eval_mrr(predict, label, at=10):
112
+ scores = base_it(predict, label, at, lambda rank: 1 / rank if rank != 0 else 0)
113
+ return {f'MRR@{at}': sum(scores) / len(scores)}
114
+
115
+
116
+ def eval_all(predict, label):
117
+ log_dict = {}
118
+ log_dict.update(eval_recall(predict, label, at=1))
119
+ log_dict.update(eval_recall(predict, label, at=5))
120
+ log_dict.update(eval_recall(predict, label, at=10))
121
+ log_dict.update(eval_mrr(predict, label, at=1))
122
+ return log_dict
123
+
124
+ def calculate_ir_metrics(true_labels: List[int], pred_labels: List[int]):
125
+ """计算信息检索中的 Precision, Recall, F1, 和 IoU。"""
126
+ true_set = set(true_labels)
127
+ pred_set = set(pred_labels)
128
+ if not true_set: return {'precision': 0.0, 'recall': 0.0, 'f1': 0.0, 'iou': 0.0}
129
+ tp = len(true_set & pred_set)
130
+ precision = tp / len(pred_set) if pred_set else 0.0
131
+ recall = tp / len(true_set)
132
+ f1 = 2 * (precision * recall) / (precision + recall) if (precision + recall) > 0 else 0.0
133
+ iou = len(true_set & pred_set) / len(true_set | pred_set) if len(true_set | pred_set) > 0 else 0.0
134
+
135
+ return {'precision': precision, 'recall': recall, 'f1': f1, 'iou': iou}
136
+
137
+ def process_results(requests: List[Dict], index_to_doc, doc_to_index):
138
+ num_error = 0
139
+ record_list = []
140
+ all_metrics = {"precision": [], "recall": [], "f1": [], "iou": []}
141
+
142
+ for request in requests:
143
+ question = request["question"]
144
+ answer = request["answer"]
145
+ generated_text = request["response"].replace('<|endoftext|>', '')
146
+ generated_text = "\nPlease answer the question based" + generated_text.split("\nPlease answer the question based")[1]
147
+
148
+ labels = [doc_to_index[txt] for txt in request["labels"]]
149
+ predictions = list(set(map(int, re.findall(r'\[(\d+)\]', generated_text))))
150
+
151
+ try:
152
+ pred_answer = generated_text.split('The answer to the question is:')[-1].split("<|im_end|>")[0].strip()
153
+ except Exception as e:
154
+ pred_answer = ""
155
+ raise ValueError(f"输出格式异常: {e}, generated_text: {generated_text}")
156
+
157
+ try:
158
+ record_list.append({
159
+ "labels_id": labels,
160
+ "pred_id": predictions,
161
+ "question": question,
162
+ "true_answer": answer,
163
+ "pred_answer": pred_answer,
164
+ "generated_text": generated_text,
165
+ "predict_context": [{i: index_to_doc[pid]} for i, pid in enumerate(predictions)],
166
+ "gt_context": [{i: index_to_doc[pid]} for i, pid in enumerate(labels)],
167
+ })
168
+ except Exception as e:
169
+ num_error += 1
170
+
171
+ metrics = calculate_ir_metrics(labels, predictions)
172
+ for k, v in metrics.items():
173
+ all_metrics[k].append(v)
174
+
175
+ metrics_dict = {k: round(float(np.mean(v)), 4) for k, v in all_metrics.items()}
176
+ print(" ==================== Retrieve metrics ======================= ")
177
+ print("AR Metrics: ", metrics_dict)
178
+ print(" ==================== Retrieve metrics ======================= ")
179
+ return {"metrics": metrics_dict, "record_list": record_list}
180
+
181
+ def parse_args():
182
+ parser = argparse.ArgumentParser()
183
+ parser.add_argument('--benchmark', type=str)
184
+ parser.add_argument('--max_batch_size', type=int)
185
+ parser.add_argument('--model_path', type=str)
186
+ parser.add_argument('--top_p', type=float, default=0.9)
187
+ parser.add_argument('--temperature', type=float, default=0.0)
188
+ parser.add_argument('--block_size', type=int, default=2048) # tokens for one memory inference
189
+ parser.add_argument('--max_chunk_per_block', type=int, default=16*1024) # chunks per block slice
190
+ parser.add_argument('--max_length', type=int, default=64) # max output length
191
+ parser.add_argument('--max_seq_len', type=int, default=0) # max input+output length
192
+ parser.add_argument('--max_query_seq_len', type=int, default=0) # max input seq len
193
+ parser.add_argument('--template', type=str, default="QWEN3_TEMPLATE")
194
+ parser.add_argument('--output_file', type=str, default="")
195
+ parser.add_argument('--case_name', type=str, default="anonymous")
196
+
197
+ args = parser.parse_args()
198
+
199
+ return args
200
+
201
+
202
+ def should_regenerate(request:dict, response: str):
203
+ """return a text if the response should be regenerated, or return None"""
204
+
205
+ if response[-len("<|object_ref_end|>"):] == "<|object_ref_end|>" and "The answer to the question is:" not in response:
206
+ if not request.get('regenerated', False):
207
+ request["regenerated"] = True
208
+ response = "\nPlease answer the question based"+response.split("\nPlease answer the question based")[1]
209
+ response = response.replace('<|endoftext|>', '')
210
+ return "<regenerate>"+ response
211
+
212
+ return None
213
+
214
+ def read_config_to_args(config_path):
215
+ with open(os.path.join(config_path, "config.json"), 'r') as f:
216
+ msa_config = json.load(f).get("msa_config")
217
+ args.doc_top_k = msa_config.get("doc_top_k", 16)
218
+ args.pooling_kernel_size = msa_config.get("pooling_kernel_size", 64)
219
+ args.router_layer_idx = msa_config.get("router_layer_idx", "all")
220
+ return args, msa_config
221
+
222
+
223
+ def msa_benchmark(args, data):
224
+
225
+ from src.msa_service import GenerateConfig, ModelConfig, MemoryConfig, MSAEngine
226
+ args, msa_config = read_config_to_args(args.model_path)
227
+
228
+ model_config = ModelConfig(model_path=args.model_path,
229
+ doc_top_k=args.doc_top_k,
230
+ pooling_kernel_size=args.pooling_kernel_size,
231
+ router_layer_idx=args.router_layer_idx,
232
+ )
233
+ generate_config = GenerateConfig(devices=list(range(torch.cuda.device_count())),
234
+ template=args.template,
235
+ max_generate_tokens=args.max_length,
236
+ max_seq_len=args.max_seq_len,
237
+ max_query_seq_len=args.max_query_seq_len,
238
+ max_batch_size=args.max_batch_size,
239
+ top_p=args.top_p,
240
+ temperature=args.temperature,
241
+ qa_mode=True)
242
+ memory_config = MemoryConfig(block_size=args.block_size,
243
+ pooling_kernel_size=args.pooling_kernel_size,
244
+ slice_chunk_size=args.max_chunk_per_block,
245
+ memory_file_path=args.memory_file,
246
+ )
247
+
248
+ final_result = {}
249
+ if args.output_file:
250
+ try:
251
+ with open(args.output_file, 'r') as f:
252
+ exist_result = json.load(f)
253
+ final_result = exist_result[args.case_name]
254
+ except:
255
+ pass
256
+
257
+ with MSAEngine(generate_config, model_config, memory_config) as engine:
258
+ print("start precision test")
259
+ idx_to_doc = engine.get_idx_to_doc()
260
+ doc_to_idx = {v: k for k, v in idx_to_doc.items()}
261
+
262
+ bsz = args.max_batch_size * generate_config.world
263
+ sorted_requests = sort_requests(data)
264
+ requests = OrderedDict({idx: item for idx, item in enumerate(sorted_requests)})
265
+
266
+ for req_idx in requests:
267
+ requests[req_idx]['idx'] = req_idx
268
+
269
+ total = len(requests)
270
+ results = [] # processed requests
271
+ pbar = tqdm(total=total, desc=f"Precision Test")
272
+
273
+ to_send = {}
274
+ while len(results) < total:
275
+ num = min(bsz-len(to_send), len(requests))
276
+ for _ in range(num):
277
+ idx, request = requests.popitem(last=False)
278
+ to_send[idx] = request
279
+
280
+ prompts, indices = [], []
281
+ for idx, request in to_send.items():
282
+ prompts.append(request.get("new_question", request["question"]))
283
+ indices.append(idx)
284
+
285
+ texts, recall_topks, _ = engine.generate(prompts, require_recall_topk=True)
286
+
287
+ for idx, response in enumerate(texts):
288
+ req_idx = indices[idx]
289
+ request = to_send[req_idx]
290
+ response = "\nPlease answer the question based"+response.split("\nPlease answer the question based")[1]
291
+ response = response.replace('<|endoftext|>', '')
292
+ new_prompt = should_regenerate(request, response)
293
+
294
+ # 判断是否需要重新生成
295
+ if new_prompt is not None:
296
+ request["new_question"] = new_prompt
297
+ else:
298
+ recall_topk = {layer: v[idx] for layer, v in recall_topks.items()}
299
+ request = to_send.pop(req_idx)
300
+ request['recall_topk'] = recall_topk
301
+ request["response"] = response
302
+ results.append(request)
303
+ pbar.update(bsz - len(to_send))
304
+ pbar.close()
305
+
306
+ assert len(results) == total, \
307
+ f"Results count mismatch: got {len(results)}, expected {total} (from query_file)"
308
+
309
+ final_result['precision'] = process_results(results, idx_to_doc, doc_to_idx)
310
+
311
+ if args.output_file:
312
+ exist_result = {}
313
+ try:
314
+ with open(args.output_file, 'r') as f:
315
+ exist_result = json.load(f)
316
+ except:
317
+ pass
318
+ exist_result[args.case_name] = final_result
319
+ with open(args.output_file, 'w') as f:
320
+ json.dump(exist_result, f, indent=4, ensure_ascii=False)
321
+ else:
322
+ s = json.dumps(final_result, indent=4, ensure_ascii=False)
323
+ print(s)
324
+
325
+ if __name__ == "__main__":
326
+ mp.set_start_method('spawn')
327
+ args = parse_args()
328
+ assert args.template in ["QWEN3_TEMPLATE", "QWEN3_INSTRUCT_TEMPLATE"]
329
+ if args.output_file:
330
+ assert args.case_name != "", "when output result to a file, please give this test case a name"
331
+
332
+ print(json.dumps(vars(args), indent=4, sort_keys=True))
333
+
334
+ data = parse_benchmark_file(args)
335
+ msa_benchmark(args, data)
src/benchmarks.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dataclasses import dataclass
3
+ from enum import Enum, auto
4
+ from typing import ClassVar
5
+
6
+ from huggingface_hub import hf_hub_download
7
+
8
+
9
+ class Category(Enum):
10
+ """Benchmark categories with associated root directories and path patterns."""
11
+ RAG = auto()
12
+ RAG_0108 = auto()
13
+ LENGTH_SCALE = auto()
14
+
15
+
16
+ # ============================================================================
17
+ # HuggingFace config & local data root
18
+ # ============================================================================
19
+
20
+ HF_REPO_ID = "Anoy123423123/MSA-RAG-BENCHMARKS"
21
+ _DATA_ROOT = os.path.join(os.getcwd(), "data")
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class BenchmarkSpec:
26
+ """Immutable specification for a single benchmark's file layout."""
27
+ bench_name: str # benchmark name, also the HF subdirectory
28
+ query_file: str
29
+ memory_file: str
30
+
31
+ def _resolve(self, filename: str) -> str:
32
+ """Return local path if cached, otherwise download from HF into data/."""
33
+ local_path = os.path.join(_DATA_ROOT, self.bench_name, filename)
34
+ if os.path.exists(local_path):
35
+ return local_path
36
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
37
+ return hf_hub_download(
38
+ repo_id=HF_REPO_ID,
39
+ filename=f"{self.bench_name}/{filename}",
40
+ repo_type="dataset",
41
+ local_dir=_DATA_ROOT,
42
+ )
43
+
44
+ @property
45
+ def query_path(self) -> str:
46
+ return self._resolve(self.query_file)
47
+
48
+ @property
49
+ def memory_path(self) -> str:
50
+ return self._resolve(self.memory_file)
51
+
52
+ def get_bench_files(self) -> tuple[str, str]:
53
+ return self.query_path, self.memory_path
54
+
55
+
56
+ # ============================================================================
57
+ # Registry: benchmark name -> spec
58
+ # ============================================================================
59
+
60
+ def _rag(name: str) -> BenchmarkSpec:
61
+ return BenchmarkSpec(name, f"qdata_{name}.pkl", f"mdata_{name}.pkl")
62
+
63
+
64
+ def _rag_0108(name: str) -> BenchmarkSpec:
65
+ return BenchmarkSpec(name, f"qdata_{name}.pkl", f"mdata_{name}.pkl")
66
+
67
+
68
+ _REGISTRY: dict[str, BenchmarkSpec] = {
69
+ # --- Length-scale benchmarks ---
70
+ "ms_100M": BenchmarkSpec("ms_100M", "qdata_msmarco_16K.pkl", "mdata_msmarco_100M.pkl"),
71
+ # --- Multi-hop QA ---
72
+ "2wikimultihopqa": _rag("2wikimultihopqa"),
73
+ "hotpotqa": _rag("hotpotqa"),
74
+ "musique": _rag("musique"),
75
+ # --- HippoRAG ---
76
+ "hipporag_narrative": _rag_0108("hipporag_narrative"),
77
+ "hipporag_popqa": _rag_0108("hipporag_popqa"),
78
+ # --- Single-hop QA ---
79
+ "nature_questions": _rag("nature_questions"),
80
+ "triviaqa_06M": _rag("triviaqa_06M"),
81
+ "triviaqa_10M": _rag("triviaqa_10M"),
82
+ # --- Multilingual / Passage retrieval ---
83
+ "dureader": _rag("dureader"),
84
+ "msmarco_v1": _rag("msmarco_v1"),
85
+ }
86
+
87
+ ALL_BENCH_NAMES: list[str] = list(_REGISTRY)
88
+
89
+
90
+ # ============================================================================
91
+ # Public API
92
+ # ============================================================================
93
+
94
+ class BenchMarks:
95
+ """Resolve benchmark name to query / memory file paths.
96
+
97
+ Usage:
98
+ bench = BenchMarks("hotpotqa")
99
+ query_file, memory_file = bench.get_bench_files()
100
+ """
101
+
102
+ AVAILABLE: ClassVar[list[str]] = ALL_BENCH_NAMES
103
+
104
+ def __init__(self, bench_name: str) -> None:
105
+ if bench_name not in _REGISTRY:
106
+ raise ValueError(
107
+ f"Unknown benchmark: {bench_name!r}. "
108
+ f"Available: {', '.join(ALL_BENCH_NAMES)}"
109
+ )
110
+ self._spec = _REGISTRY[bench_name]
111
+ self.name = bench_name
112
+ self.bench_name = self._spec.bench_name
113
+ self.query_file_name = self._spec.query_file
114
+ self.memory_file_name = self._spec.memory_file
115
+
116
+ def get_bench_files(self) -> tuple[str, str]:
117
+ return self._spec.get_bench_files()
118
+
119
+ def __repr__(self) -> str:
120
+ return f"BenchMarks({self.name!r})"
src/config/memory_config.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Union, Dict, List
2
+ from dataclasses import dataclass
3
+
4
+ from src.utils.template import QWEN3_TEMPLATE, QWEN3_INSTRUCT_TEMPLATE
5
+
6
+ @dataclass
7
+ class GenerateConfig:
8
+ devices: List[int] = None
9
+ template: Union[str, Dict] = None
10
+ max_generate_tokens: int = 256
11
+ max_seq_len: int = 0 # total sequence length in a batch
12
+ max_query_seq_len: int = 0 # max sequence for a single query
13
+ max_batch_size: int = 0 # 0 if batch size is not limited
14
+ top_p: float = 0.9
15
+ temperature: float = 0.0
16
+ qa_mode: bool = False
17
+
18
+ def __post_init__(self):
19
+ if isinstance(self.template, str):
20
+ assert self.template in ["QWEN3_TEMPLATE", "QWEN3_INSTRUCT_TEMPLATE"]
21
+ self.template = eval(self.template)
22
+ assert isinstance(self.template, dict)
23
+
24
+ @property
25
+ def world(self):
26
+ return len(self.devices) if self.devices else 0
27
+
28
+ @dataclass
29
+ class ModelConfig:
30
+ model_path: str = "Anoy123423123/MSA-4B"
31
+
32
+ doc_top_k: int = 16
33
+ pooling_kernel_size: int = 64
34
+ router_layer_idx: str = "all"
35
+
36
+ # template
37
+ template_token_id = -2
38
+ template_id_num = 3
39
+
40
+ def get_model_envs(self):
41
+ envs = {}
42
+ # envs["TOP_K_DOCS"] = str(self.doc_top_k)
43
+ # envs["POOLING_KERNEL_SIZE"] = str(self.pooling_kernel_size)
44
+ # envs["ROUTER_LAYER_IDX"] = self.router_layer_idx
45
+ return envs
46
+
47
+
48
+ @dataclass
49
+ class MemoryConfig:
50
+ block_size: int = 16000 # 当对 memory 进行推理时使用的分块大小(tokens)
51
+ slice_chunk_size: int = 16 * 1024
52
+ pooling_kernel_size: int = 64
53
+ memory_file_path: str = ""
54
+
55
+ socket_ip: str = ""
56
+ socket_port: int = 0
src/evaluation/llm_judge.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ import os
3
+ from tqdm import tqdm
4
+ import sys
5
+ import json
6
+ import numpy as np
7
+ from glob import glob
8
+ from concurrent.futures import ThreadPoolExecutor
9
+
10
+ def parse_match_result(text):
11
+ text_lower = text.strip().lower()
12
+ if 'match' in text_lower and 'mismatch' not in text_lower:
13
+ return 1
14
+ return 0
15
+
16
+ def build_match_prompt(gold_answer, model_answer):
17
+ return f"""You are a strict but fair evaluator.
18
+
19
+ Judge whether the Generated Answer correctly includes
20
+ the core meaning of the Reference Answer.
21
+
22
+ - The Generated Answer may add extra details.
23
+ - Do NOT penalize additional information.
24
+ - The core concept in the Reference Answer must be present
25
+ or clearly implied.
26
+
27
+
28
+ Reference Answer:
29
+ {gold_answer}
30
+
31
+ Generated Answer:
32
+ {model_answer}
33
+
34
+ Output:
35
+ match or mismatch"""
36
+
37
+
38
+ def build_score_prompt(gold_answer, model_answer, query):
39
+ return f""""Based on the accuracy, completeness, and relevance of the predicted answer to the real answer in the context of the **query**, assign an objective score from 0 to 5 (5 being the highest, 0 the lowest).
40
+
41
+ The scoring must strictly adhere to the following criteria. The final output can only be a single number.
42
+
43
+ Scoring Criteria:
44
+
45
+ 5: The predicted answer is exactly the same as the real answer and correctly answers the query. Differences in wording do not affect factual accuracy.
46
+
47
+ 4: The predicted answer contains all the core information of the real answer, with no errors, but includes a small amount of non-critical redundant content.
48
+
49
+ 3: The predicted answer captures the core information but differs from the real answer in some aspects. The predicted answer is slightly incomplete or imprecise, but contains no errors.
50
+
51
+ 2: The predicted answer is partially relevant to the real answer but omits a significant amount of information or deviates from the core topic of the query.
52
+
53
+ 1: The predicted answer attempts to address the query (maintains basic relevance to the topic) but provides factually incorrect information. It does not contradict the core claim of the real answer, but shows incomplete or inaccurate understanding of the topic.
54
+
55
+ 0. The predicted answer is completely unrelated to the query, consists of gibberish, or is a pure hallucination that shares no logical connection with the real answer.
56
+
57
+ Query:
58
+
59
+ {query}
60
+
61
+ True Answer:
62
+
63
+ {gold_answer}
64
+
65
+ Predicted Answer:
66
+
67
+ {model_answer}
68
+
69
+ Output only a single number (0, 1, 2, 3, 4, or 5): """
70
+
71
+
72
+
73
+ def parse_score_result(text):
74
+ """解析 LLM 返回的评分结果,返回 0-5 的分数"""
75
+ text = text.strip()
76
+ # 尝试直接解析数字
77
+ for char in text:
78
+ if char.isdigit() and int(char) <= 5:
79
+ return int(char)
80
+ # 如果没有找到有效数字,返回 0
81
+ return 0
82
+
83
+ def get_eval_response(prompt):
84
+ try:
85
+ completion = client.chat.completions.create(
86
+ extra_headers={
87
+ },
88
+ extra_body={},
89
+ model='google/gemini-2.5-flash',
90
+ messages=[
91
+ {
92
+ "role": "user",
93
+ "content": [
94
+ {
95
+ "type": "text",
96
+ "text": prompt
97
+ },
98
+ ]
99
+ }
100
+ ],
101
+ temperature=0.0
102
+ )
103
+ return completion.choices[0].message.content
104
+ except Exception as e:
105
+ print(e)
106
+ return ''
107
+
108
+ if __name__ == "__main__":
109
+ dirname = sys.argv[1]
110
+ openrouter_api_key = os.environ.get("OPENROUTER_API_KEY", "")
111
+ if not openrouter_api_key:
112
+ raise ValueError("Please set the OPENROUTER_API_KEY environment variable.")
113
+ client = OpenAI(
114
+ base_url="https://openrouter.ai/api/v1",
115
+ api_key=openrouter_api_key,
116
+ )
117
+ JUDGE_MODEL = 'google/gemini-2.5-flash'
118
+ if dirname.endswith(".json") and os.path.isfile(dirname):
119
+ json_dir = os.path.split(dirname)[0]
120
+ json_paths = [dirname]
121
+ elif os.path.exists(dirname) and os.path.isdir(dirname):
122
+ json_dir = dirname
123
+ json_paths = glob(f'{json_dir}/*.json')
124
+ else:
125
+ json_dir = f"src/evaluation/outputs/{dirname}"
126
+ json_paths = glob(f'{json_dir}/*.json')
127
+
128
+ for json_path in tqdm(json_paths, total=len(json_paths)):
129
+ if 'score' in json_path:
130
+ continue
131
+ json_name = os.path.split(json_path)[-1].split('.')[0]
132
+ with open(json_path, 'r') as f:
133
+ datas = json.load(f)["anonymous"]["precision"]
134
+ print(datas["metrics"])
135
+
136
+ final_dict = dict()
137
+ final_dict['metrics'] = datas["metrics"]
138
+ final_dict['record_list'] = []
139
+ pred_answers = []
140
+ true_answers = []
141
+ questions = []
142
+ pred_contexts = []
143
+ true_contexts = []
144
+ pred_ids = []
145
+ labels_ids = []
146
+
147
+ for dic in datas['record_list']:
148
+ question = dic['question']
149
+ questions.append(question)
150
+ true_answer = dic['true_answer']
151
+ key_word = "response" if "response" in dic else "pred_answer"
152
+ pred_answer = dic[key_word].replace("<|im_end|>",'').replace("<|endoftext|>",'')
153
+
154
+ pred_ids.append(dic['pred_id'])
155
+ labels_ids.append(dic['labels_id'])
156
+
157
+ pred_con_li = [v for di in dic['predict_context'] for k,v in di.items()]
158
+ true_con_li = [v for di in dic['gt_context'] for k,v in di.items()]
159
+ pred_contexts.append(pred_con_li)
160
+ true_contexts.append(true_con_li)
161
+
162
+ true_answers.append(true_answer)
163
+ qa = pred_answer.split('<answer>')[-1]
164
+ try:
165
+ q = qa.split("The answer to the question is: ")[-2].split("The user's question is: ")[-1].replace('\n<|object_ref_end|>','')
166
+ except:
167
+ q = ''
168
+ a = qa.split("The answer to the question is: ")[-1].replace('</answer>','').replace("The user's question is:",'')
169
+ a = a.split('</think>')[-1]
170
+ if "Answer:" in a:
171
+ a = a.split("Answer:")[1].strip()
172
+ pred_answers.append(a)
173
+
174
+ match_prompts = []
175
+ score_prompts = []
176
+ for idx in tqdm(range(len(pred_answers))):
177
+ true_answer = true_answers[idx]
178
+ pred_answer = pred_answers[idx]
179
+ question = questions[idx]
180
+ match_prompt = build_match_prompt(true_answer, pred_answer)
181
+ score_prompt = build_score_prompt(true_answer, pred_answer, question)
182
+ match_prompts.append(match_prompt)
183
+ score_prompts.append(score_prompt)
184
+
185
+
186
+ with ThreadPoolExecutor(max_workers=32) as executor:
187
+ score_results = list(tqdm(executor.map(get_eval_response, score_prompts), total=len(score_prompts), desc="Tokenizing texts"))
188
+
189
+ match_final_scores = []
190
+ score_final_scores = []
191
+ for idx in range(len(score_results)):
192
+ score_result = score_results[idx]
193
+ score_score = parse_score_result(score_result)
194
+ score_final_scores.append(score_score)
195
+
196
+
197
+ match_final_scores_mean = np.mean(match_final_scores)
198
+ score_final_scores_mean = np.mean(score_final_scores)
199
+ print(" ======================================= ")
200
+ print(f"{json_name} LLM Score: {score_final_scores_mean:.4f}")
201
+ print(" ======================================= ")
202
+
203
+ old_file_path = json_path
204
+ new_file_path = json_path.replace(".json", f"_score{str(score_final_scores_mean).replace('.','point')[:9]}.json")
205
+ os.rename(old_file_path, new_file_path)
206
+
207
+ # final_dict
208
+ for idx in range(len(questions)):
209
+ question = questions[idx]
210
+ answer = true_answers[idx]
211
+ pred_answer = pred_answers[idx]
212
+ score = score_final_scores[idx]
213
+ final_dict['record_list'].append({'question':question, 'true_answer':answer, 'pred_answer':pred_answer, 'pred_score':score})
214
+ final_dict["score_final_scores_mean"] = score_final_scores_mean
215
+ new_file_path = f'{json_dir}/{json_name}_llmscore.json'
216
+
217
+ with open(new_file_path, 'w',encoding='utf-8') as f:
218
+ json.dump(final_dict, f, indent=4,ensure_ascii=False)
src/msa/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .generate import MSAGenerationMixin
2
+ from .memory_sparse_attention import MemorySparseAttention
3
+ from .configuration_msa import MSAConfig
src/msa/configuration_msa.py ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MSA (Memory Sparse Attention) Configuration"""
2
+
3
+ from transformers.models.qwen3.configuration_qwen3 import Qwen3Config
4
+
5
+
6
+ class DotDict(dict):
7
+ """支持点号访问的字典类"""
8
+ __getattr__ = dict.get
9
+ __setattr__ = dict.__setitem__
10
+ __delattr__ = dict.__delitem__
11
+
12
+ def __getstate__(self):
13
+ return dict(self)
14
+
15
+ def __setstate__(self, state):
16
+ self.update(state)
17
+
18
+
19
+ class MSAConfig(Qwen3Config):
20
+ """
21
+ MSA 模型的配置类,继承自 Qwen3Config。
22
+
23
+ 主要功能:确保 msa_config 在加载时自动转换为 DotDict,
24
+ 支持使用点号访问属性(如 config.msa_config.pad_free)
25
+ """
26
+
27
+ model_type = "msa"
28
+
29
+ def __init__(self, msa_config=None, **kwargs):
30
+ super().__init__(**kwargs)
31
+ if msa_config is not None:
32
+ self.msa_config = DotDict(msa_config) if not isinstance(msa_config, DotDict) else msa_config
33
+
34
+ def __setattr__(self, name, value):
35
+ """重写 __setattr__,确保设置 msa_config 时自动转换为 DotDict"""
36
+ if name == "msa_config" and isinstance(value, dict) and not isinstance(value, DotDict):
37
+ value = DotDict(value)
38
+ super().__setattr__(name, value)
39
+
40
+ @classmethod
41
+ def from_dict(cls, config_dict, **kwargs):
42
+ """
43
+ 从字典创建配置对象时,确保 msa_config 被转换为 DotDict。
44
+ 这是关键方法,AutoConfig.from_pretrained() 最终会调用这个方法。
45
+ """
46
+ # 先调用父类的 from_dict
47
+ config = super().from_dict(config_dict, **kwargs)
48
+
49
+ # 确保 msa_config 是 DotDict
50
+ if hasattr(config, 'msa_config') and isinstance(config.msa_config, dict) and not isinstance(config.msa_config, DotDict):
51
+ config.msa_config = DotDict(config.msa_config)
52
+
53
+ return config
src/msa/generate.py ADDED
@@ -0,0 +1,354 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import re
4
+ from typing import Optional, Union
5
+
6
+ import torch
7
+ from torch import nn
8
+ from transformers.cache_utils import Cache
9
+ from transformers.generation.configuration_utils import GenerationConfig
10
+ from transformers.generation.logits_process import LogitsProcessorList
11
+ from transformers.generation.stopping_criteria import StoppingCriteriaList
12
+ from transformers.generation.streamers import BaseStreamer
13
+ from transformers.generation.utils import (
14
+ GenerateDecoderOnlyOutput,
15
+ GenerateEncoderDecoderOutput,
16
+ GenerateNonBeamOutput,
17
+ GenerationMixin,
18
+ )
19
+
20
+ logger = logging.getLogger(__name__)
21
+
22
+ class MSAGenerationMixin(GenerationMixin):
23
+ def _sample(
24
+ self,
25
+ input_ids: torch.LongTensor,
26
+ logits_processor: LogitsProcessorList,
27
+ stopping_criteria: StoppingCriteriaList,
28
+ generation_config: GenerationConfig,
29
+ synced_gpus: bool,
30
+ streamer: Optional["BaseStreamer"],
31
+ **model_kwargs,
32
+ ) -> Union[GenerateNonBeamOutput, torch.LongTensor]:
33
+ # init values
34
+ pad_token_id = generation_config._pad_token_tensor
35
+ output_attentions = generation_config.output_attentions
36
+ output_hidden_states = generation_config.output_hidden_states
37
+ output_scores = generation_config.output_scores
38
+ output_logits = generation_config.output_logits
39
+ return_dict_in_generate = generation_config.return_dict_in_generate
40
+ has_eos_stopping_criteria = any(hasattr(criteria, "eos_token_id") for criteria in stopping_criteria)
41
+ do_sample = generation_config.do_sample
42
+
43
+ # init attention / hidden states / scores tuples
44
+ scores = () if (return_dict_in_generate and output_scores) else None
45
+ raw_logits = () if (return_dict_in_generate and output_logits) else None
46
+ decoder_attentions = () if (return_dict_in_generate and output_attentions) else None
47
+ cross_attentions = () if (return_dict_in_generate and output_attentions) else None
48
+ decoder_hidden_states = () if (return_dict_in_generate and output_hidden_states) else None
49
+
50
+ # if model is an encoder-decoder, retrieve encoder attention weights and hidden states
51
+ if return_dict_in_generate and self.config.is_encoder_decoder:
52
+ encoder_attentions = model_kwargs["encoder_outputs"].get("attentions") if output_attentions else None
53
+ encoder_hidden_states = (
54
+ model_kwargs["encoder_outputs"].get("hidden_states") if output_hidden_states else None
55
+ )
56
+
57
+ # keep track of which sequences are already finished
58
+ batch_size, cur_len = input_ids.shape
59
+ this_peer_finished = False
60
+ unfinished_sequences = torch.ones(batch_size, dtype=torch.long, device=input_ids.device)
61
+ model_kwargs = self._get_initial_cache_position(input_ids, model_kwargs)
62
+
63
+ model_forward = self.__call__
64
+ if isinstance(model_kwargs.get("past_key_values"), Cache):
65
+ is_compileable = model_kwargs["past_key_values"].is_compileable and self._supports_static_cache
66
+ if getattr(self, "hf_quantizer", None) is not None:
67
+ is_compileable &= self.hf_quantizer.is_compileable
68
+ is_compileable = is_compileable and not generation_config.disable_compile
69
+ if is_compileable and (
70
+ self.device.type == "cuda" or generation_config.compile_config._compile_all_devices
71
+ ):
72
+ os.environ["TOKENIZERS_PARALLELISM"] = "0"
73
+ model_forward = self.get_compiled_call(generation_config.compile_config)
74
+
75
+ if generation_config.prefill_chunk_size is not None:
76
+ model_kwargs = self._prefill_chunking(input_ids, generation_config, **model_kwargs)
77
+ is_prefill = False
78
+ else:
79
+ is_prefill = True
80
+
81
+ meta = model_kwargs["past_key_values"].meta
82
+ max_generate_tokens = model_kwargs["past_key_values"].meta["max_generate_tokens"]
83
+ tokenizer = meta['tokenizer']
84
+ response_string = meta["response_string"]
85
+ idx_to_doc = meta["idx_to_doc"]
86
+ pattern = meta["pattern"]
87
+ retrieval_end_flags = torch.zeros(batch_size, dtype=torch.bool, device=input_ids.device)
88
+ round_end_flags = torch.zeros(batch_size, dtype=torch.bool, device=input_ids.device)
89
+ inner_string = ["" for _ in range(batch_size)]
90
+ source_context_copied = False
91
+ all_input_str = [""] * batch_size
92
+ last_valid_inputs = input_ids[:, -1:].clone().to(input_ids.device)
93
+ is_first = 1
94
+ generate_stage = 1
95
+ cnt = 0
96
+ all_model_inputs = {}
97
+ has_generate_stage3 = False
98
+ first_stage2 = True
99
+ round_end = False
100
+
101
+ while self._has_unfinished_sequences(this_peer_finished, synced_gpus, device=input_ids.device):
102
+ if "position_ids" in model_kwargs:
103
+ position_ids = model_kwargs.pop("position_ids")
104
+ else:
105
+ position_ids = (position_ids[:, -1:] + model_kwargs["attention_mask"]).to(input_ids.device)
106
+
107
+ model_inputs = model_kwargs.copy()
108
+
109
+ if is_first == 1:
110
+ model_inputs.update({"input_ids": input_ids})
111
+ else:
112
+ model_inputs.update({"input_ids": last_valid_inputs})
113
+
114
+ model_inputs.update({"position_ids": position_ids})
115
+ model_inputs.update({"output_attentions": output_attentions} if output_attentions else {})
116
+ model_inputs.update({"output_hidden_states": output_hidden_states} if output_hidden_states else {})
117
+
118
+ input_str = tokenizer.batch_decode(model_inputs['input_ids'])
119
+ all_input_str = [s + input_str[i] for i, s in enumerate(all_input_str)]
120
+
121
+ if not is_prefill:
122
+ for layer_idx in range(self.config.num_hidden_layers):
123
+ model_inputs["past_key_values"].record_kwargs(layer_idx, {"stage": "generate"})
124
+
125
+ if 'doc_ids' not in all_model_inputs:
126
+ all_model_inputs['doc_ids'] = model_inputs['doc_ids'].clone().to(input_ids.device)
127
+ else:
128
+ all_model_inputs['doc_ids'] = torch.cat((all_model_inputs['doc_ids'], model_inputs['doc_ids'][:, -model_inputs['attention_mask'].shape[1]:]), dim=1)
129
+
130
+ all_model_inputs['attention_mask'] = torch.cat((all_model_inputs['attention_mask'], model_inputs['attention_mask']), dim=1) if 'attention_mask' in all_model_inputs else model_inputs['attention_mask'].clone().to(input_ids.device)
131
+ all_model_inputs['input_ids'] = torch.cat((all_model_inputs['input_ids'], model_inputs['input_ids']), dim=1) if 'input_ids' in all_model_inputs else model_inputs['input_ids'].clone().to(input_ids.device)
132
+ all_model_inputs['position_ids'] = torch.cat((all_model_inputs['position_ids'], model_inputs['position_ids']), dim=1) if 'position_ids' in all_model_inputs else model_inputs['position_ids'].clone().to(input_ids.device)
133
+ all_model_inputs['past_key_values'] = model_inputs.get('past_key_values')
134
+ all_model_inputs['cache_position'] = model_inputs['cache_position'].clone().to(input_ids.device)
135
+
136
+ if round_end:
137
+ break
138
+
139
+ if is_prefill:
140
+ outputs = self(**all_model_inputs, return_dict=True)
141
+ is_prefill = False
142
+ first_stage2 = False
143
+ else:
144
+ outputs = model_forward(**model_inputs, return_dict=True)
145
+ is_first = 0
146
+
147
+ # update model kwargs for next generation step
148
+ model_kwargs = self._update_model_kwargs_for_generation(
149
+ outputs,
150
+ model_kwargs,
151
+ is_encoder_decoder=self.config.is_encoder_decoder,
152
+ )
153
+ if synced_gpus and this_peer_finished:
154
+ continue
155
+
156
+ next_token_logits = outputs.logits[:, -1, :].to(copy=True, dtype=torch.float32, device=input_ids.device)
157
+
158
+ next_token_scores = logits_processor(last_valid_inputs, next_token_logits)
159
+
160
+ # Store scores, attentions and hidden_states when required
161
+ if return_dict_in_generate:
162
+ if output_scores:
163
+ scores += (next_token_scores,)
164
+ if output_logits:
165
+ raw_logits += (next_token_logits,)
166
+ if output_attentions:
167
+ decoder_attentions += (
168
+ (outputs.decoder_attentions,) if self.config.is_encoder_decoder else (outputs.attentions,)
169
+ )
170
+ if self.config.is_encoder_decoder:
171
+ cross_attentions += (outputs.cross_attentions,)
172
+ if output_hidden_states:
173
+ decoder_hidden_states += (
174
+ (outputs.decoder_hidden_states,)
175
+ if self.config.is_encoder_decoder
176
+ else (outputs.hidden_states,)
177
+ )
178
+
179
+ # token selection
180
+ if do_sample:
181
+ probs = nn.functional.softmax(next_token_scores, dim=-1)
182
+ next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
183
+ else:
184
+ next_tokens = torch.argmax(next_token_scores, dim=-1)
185
+ if has_eos_stopping_criteria:
186
+ next_tokens = next_tokens * unfinished_sequences + pad_token_id * (1 - unfinished_sequences)
187
+
188
+ cur_generate_context = tokenizer.batch_decode(next_tokens)
189
+
190
+ if generate_stage == 2 or generate_stage == 3:
191
+ source_context_list = []
192
+ imstart_str = "<|im_start|>"
193
+
194
+ if generate_stage == 2:
195
+ for i, response_sample in enumerate(inner_string):
196
+ if '<End-of-Retrieve>' in response_sample:
197
+ question = all_input_str[i].split('historical document information\n\n')[-1]
198
+ question = question.split('\nPlease return all documents related to the question')[0]
199
+ source_context_list.append(imstart_str + 'The user\'s question is: %s\n<|object_ref_end|>' % (question))
200
+ else:
201
+ result = re.findall(pattern, response_sample)
202
+ indices = sorted(list(set(map(int, result))))
203
+ indices = [idx for idx in indices if idx in idx_to_doc]
204
+ try:
205
+ response_doc_str = ''.join(f"[{idx}]. {idx_to_doc[idx]}\n" for idx in indices)
206
+ response_doc_str = response_doc_str + '<|object_ref_end|>'
207
+ except KeyError as e:
208
+ logger.warning("Document not found for index %s, available indices: %s", e, indices)
209
+ response_doc_str = "" + '<|object_ref_end|>'
210
+ source_context_list.append(response_doc_str)
211
+
212
+ if generate_stage == 3:
213
+ for i, response_sample in enumerate(response_string):
214
+ if '<End-of-Retrieve>' in response_sample:
215
+ question = all_input_str[i].split('historical document information\n\n')[-1]
216
+ question = question.split('\nPlease return all documents related to the question')[0]
217
+ source_context_list.append(imstart_str + 'The user\'s question is: %s\n<|object_ref_end|>' % (question))
218
+ else:
219
+ source_context_list.append("")
220
+
221
+ source_batch = tokenizer(
222
+ source_context_list,
223
+ padding="longest",
224
+ truncation=True,
225
+ return_tensors="pt",
226
+ add_special_tokens=True,
227
+ padding_side="left",
228
+ )
229
+
230
+ sh = source_batch['input_ids'].shape
231
+
232
+ batch_source_input_ids = source_batch['input_ids'].clone().detach().long().to(input_ids.device)
233
+ batch_source_attn_mask = source_batch['attention_mask'].clone().detach().long().to(input_ids.device)
234
+ batch_source_doc_ids = torch.zeros_like(source_batch['input_ids'], dtype=torch.long, device=input_ids.device)
235
+ batch_source_position_ids = torch.arange(sh[1], dtype=torch.long, device=input_ids.device).unsqueeze(0).expand(sh[0], -1)
236
+ batch_source_position_ids = batch_source_position_ids + model_inputs['position_ids'] + torch.sum(batch_source_attn_mask, dim=1, keepdim=True) - sh[1] + 1
237
+
238
+ input_ids = batch_source_input_ids
239
+ model_kwargs['attention_mask'] = batch_source_attn_mask
240
+ model_kwargs['doc_ids'] = batch_source_doc_ids
241
+ model_kwargs['position_ids'] = batch_source_position_ids
242
+ source_context_copied = True
243
+
244
+ cur_len += sh[1]
245
+ this_peer_finished = False
246
+ del outputs
247
+ is_first = 1
248
+ inner_string = ["" for _ in range(batch_size)]
249
+
250
+ if generate_stage == 2:
251
+ round_end = True
252
+
253
+ if generate_stage == 2:
254
+ generate_stage = 1
255
+
256
+ if generate_stage == 3:
257
+ generate_stage = 4
258
+
259
+ round_end_flags = torch.zeros(batch_size, dtype=torch.bool, device=input_ids.device)
260
+
261
+ else:
262
+ input_ids = torch.cat([input_ids, next_tokens[:, None]], dim=-1)
263
+ if True:
264
+ temp = stopping_criteria(input_ids[:, -1:], scores)
265
+ unfinished_sequences = unfinished_sequences & ~temp
266
+ # max_res_len = 3000
267
+ # if max([len(n) for n in response_string]) > max_res_len:
268
+ # unfinished_sequences = torch.zeros_like(unfinished_sequences)
269
+
270
+ this_peer_finished = unfinished_sequences.max() == 0
271
+ if this_peer_finished and not source_context_copied:
272
+ logger.warning("Generation finished before source context was copied")
273
+
274
+ if streamer is not None:
275
+ streamer.put(next_tokens.cpu())
276
+ cur_len += 1
277
+ del outputs
278
+ input_ids = input_ids[:, -1:]
279
+ model_kwargs['doc_ids'] = torch.nn.functional.pad(model_kwargs['doc_ids'], (0, 1), value=-1)
280
+
281
+ model_kwargs["attention_mask"] = torch.ones(batch_size, 1, dtype=torch.long, device=input_ids.device)
282
+ if generate_stage == 1:
283
+ for i, retrieval_end_flag in enumerate(retrieval_end_flags):
284
+ if retrieval_end_flag or round_end_flags[i]:
285
+ model_kwargs["attention_mask"][i, 0] = 0
286
+ else:
287
+ last_valid_inputs[i, -1] = input_ids[i, -1]
288
+ else:
289
+ for i, retrieval_end_flag in enumerate(retrieval_end_flags):
290
+ last_valid_inputs[i, -1] = input_ids[i, -1]
291
+
292
+ assert len(cur_generate_context) == len(response_string)
293
+ for i in range(len(cur_generate_context)):
294
+ response_string[i] += cur_generate_context[i]
295
+ inner_string[i] += cur_generate_context[i]
296
+ round_end_flags[i] |= "<|object_ref_end|>" in inner_string[i]
297
+ retrieval_end_flags[i] |= '<End-of-Retrieve>' in response_string[i]
298
+ if source_context_copied and generate_stage == 1:
299
+ mypattern = r"^\[\d*\]?$"
300
+ is_id = bool(re.fullmatch(mypattern, inner_string[i]))
301
+ is_EOR = inner_string[i] == '<End-of-Retrieve>'[:len(inner_string[i])]
302
+ round_end_flags[i] |= (not is_id and not is_EOR)
303
+
304
+ if not source_context_copied:
305
+ max_ret_len = 1000
306
+ retrieval_end_flags[i] |= len(response_string[i]) > max_ret_len
307
+
308
+ if sum(round_end_flags * unfinished_sequences * (~retrieval_end_flags)) == sum(unfinished_sequences * (~retrieval_end_flags)) and not has_generate_stage3:
309
+ generate_stage = 2
310
+ for i in range(len(cur_generate_context)):
311
+ if source_context_copied and round_end_flags[i]:
312
+ if "<|object_ref_end|>" not in inner_string[i] or not bool(re.fullmatch(mypattern, inner_string[i].split("<|object_ref_end|>")[0])):
313
+ retrieval_end_flags[i] = True
314
+ if sum(retrieval_end_flags * unfinished_sequences) == sum(unfinished_sequences) and not has_generate_stage3:
315
+ generate_stage = 3
316
+ has_generate_stage3 = True
317
+ cnt += 1
318
+ if cnt > max_generate_tokens:
319
+ break
320
+
321
+ all_model_inputs['attention_mask'], indices = all_model_inputs['attention_mask'].sort(dim=1, descending=False, stable=True)
322
+ all_model_inputs['input_ids'] = all_model_inputs['input_ids'].gather(dim=1, index=indices)
323
+ all_model_inputs['position_ids'] = all_model_inputs['position_ids'].gather(dim=1, index=indices)
324
+ all_model_inputs['doc_ids'] = all_model_inputs['doc_ids'].gather(dim=1, index=indices)
325
+
326
+ input_ids = all_model_inputs['input_ids']
327
+
328
+ if streamer is not None:
329
+ streamer.end()
330
+
331
+ if return_dict_in_generate:
332
+ if self.config.is_encoder_decoder:
333
+ return GenerateEncoderDecoderOutput(
334
+ sequences=input_ids,
335
+ scores=scores,
336
+ logits=raw_logits,
337
+ encoder_attentions=encoder_attentions,
338
+ encoder_hidden_states=encoder_hidden_states,
339
+ decoder_attentions=decoder_attentions,
340
+ cross_attentions=cross_attentions,
341
+ decoder_hidden_states=decoder_hidden_states,
342
+ past_key_values=model_kwargs.get("past_key_values"),
343
+ )
344
+ else:
345
+ return GenerateDecoderOnlyOutput(
346
+ sequences=input_ids,
347
+ scores=scores,
348
+ logits=raw_logits,
349
+ attentions=decoder_attentions,
350
+ hidden_states=decoder_hidden_states,
351
+ past_key_values=model_kwargs.get("past_key_values"),
352
+ )
353
+ else:
354
+ return input_ids
src/msa/memory_sparse_attention.py ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ from typing import Optional, Tuple
6
+
7
+ from transformers.models.qwen3.modeling_qwen3 import (
8
+ Qwen3Attention,
9
+ Qwen3Config,
10
+ apply_rotary_pos_emb,
11
+ repeat_kv,
12
+ )
13
+ try:
14
+ from flash_attn import flash_attn_varlen_func
15
+ except ImportError:
16
+ print("请安装flash-attn库: pip install flash-attn --no-build-isolation")
17
+ flash_attn_varlen_func = None
18
+
19
+
20
+ class MemorySparseAttention(Qwen3Attention):
21
+ def __init__(self, config: Qwen3Config, layer_idx: int):
22
+ super().__init__(config=config, layer_idx=layer_idx)
23
+ if flash_attn_varlen_func is None:
24
+ raise ImportError("flash_attn is required. Please install it via 'pip install flash-attn --no-build-isolation'")
25
+
26
+ self.layer_idx = layer_idx
27
+ self.top_k_docs = config.msa_config.top_k_docs
28
+ self.pooling_kernel_size = config.msa_config.pooling_kernel_size
29
+ self.router_layer_idx = config.msa_config.router_layer_idx
30
+
31
+ if self.router_layer_idx == "all":
32
+ self.router_layer_idx = list(range(config.num_hidden_layers))
33
+ else:
34
+ self.router_layer_idx = [int(i) for i in self.router_layer_idx.split(",")]
35
+ self.is_router_layer = self.layer_idx in self.router_layer_idx
36
+
37
+ self.head_reduce_method = config.msa_config.head_reduce_method
38
+ self.query_reduce_method = config.msa_config.query_reduce_method
39
+ self.chunk_reduce_method = config.msa_config.chunk_reduce_method
40
+ self.decouple_pooling_mode = config.msa_config.decouple_pooling_mode
41
+ self.aux_loss_method = config.msa_config.aux_loss_method
42
+
43
+ self.decouple_router = config.msa_config.decouple_router
44
+ if self.is_router_layer and self.decouple_router:
45
+ self.router_k_proj = nn.Sequential(
46
+ nn.Linear(config.hidden_size, config.num_key_value_heads * self.head_dim, bias=False),
47
+ # nn.GELU(),
48
+ # nn.Linear(config.num_key_value_heads * self.head_dim, config.num_key_value_heads * self.head_dim, bias=False)
49
+ )
50
+ self.router_q_proj = nn.Sequential(
51
+ nn.Linear(config.hidden_size, config.num_attention_heads * self.head_dim, bias=False),
52
+ # nn.GELU(),
53
+ # nn.Linear(config.num_attention_heads * self.head_dim, config.num_attention_heads * self.head_dim, bias=False)
54
+ )
55
+ self.num_kv_heads = config.num_key_value_heads
56
+
57
+ self.sliding_window = None
58
+ self.selected_docs_indices = None
59
+ self.max_doc_id = None
60
+ self.num_split_for_kv = 8
61
+ self.template_prefix_kcache = None
62
+ self.template_prefix_vcache = None
63
+ self.memory_client = None
64
+
65
+ def set_memory_client(self, memory_client):
66
+ self.memory_client = memory_client
67
+
68
+ def forward(
69
+ self,
70
+ hidden_states: torch.Tensor,
71
+ doc_ids: torch.LongTensor,
72
+ attention_mask: Optional[torch.Tensor] = None,
73
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
74
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
75
+ **kwargs,
76
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
77
+
78
+ if self.training:
79
+ return self._forward(
80
+ hidden_states,
81
+ doc_ids,
82
+ attention_mask,
83
+ position_embeddings,
84
+ past_key_value,
85
+ **kwargs,
86
+ )
87
+ elif past_key_value is not None:
88
+ return self.forward_with_kvcache_for_batch_parrallel(
89
+ hidden_states,
90
+ doc_ids,
91
+ attention_mask,
92
+ position_embeddings,
93
+ past_key_value,
94
+ **kwargs,
95
+ )
96
+ else:
97
+ raise Exception("error!")
98
+
99
+ @staticmethod
100
+ def map_tensor_to_group_ids(a: torch.Tensor) -> torch.Tensor:
101
+ if a.ndim != 1:
102
+ raise ValueError("输入 Tensor a 必须是一维的。")
103
+
104
+ diff_mask = torch.diff(a) != 0 # [L-1]
105
+ id_increments = diff_mask.int() # [L-1]
106
+ group_indices_offset = torch.cumsum(id_increments, dim=0) # [L-1]
107
+
108
+ b = torch.cat((
109
+ torch.tensor([0], device=a.device, dtype=a.dtype),
110
+ group_indices_offset
111
+ )) + 1
112
+
113
+ return b
114
+
115
+ def forward_with_kvcache_for_batch_parrallel(
116
+ self,
117
+ hidden_states: torch.Tensor,
118
+ doc_ids: torch.LongTensor,
119
+ attention_mask: Optional[torch.Tensor] = None,
120
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
121
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
122
+ **kwargs,
123
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
124
+
125
+ bsz, q_len, _ = hidden_states.shape
126
+ device, dtype = hidden_states.device, hidden_states.dtype
127
+ hidden_shape = (bsz, q_len, -1, self.head_dim)
128
+
129
+ query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
130
+ key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
131
+ value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
132
+ cos, sin = position_embeddings
133
+ query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
134
+
135
+ stage = past_key_value.cache_kwargs[self.layer_idx]["stage"]
136
+
137
+ if stage == "prefill_stage1":
138
+ max_doc_id = int(doc_ids.max().item())
139
+ doc_token_mask = (doc_ids > 0) & (attention_mask == 1)
140
+ doc_indices = torch.nonzero(doc_token_mask, as_tuple=False)
141
+ original_doc_ids = doc_ids[doc_token_mask]
142
+ original_doc_batch_indices = doc_indices[:, 0]
143
+ global_doc_ids = original_doc_batch_indices * (max_doc_id + 1) + original_doc_ids
144
+
145
+ if self.is_router_layer:
146
+ _, counts = torch.unique_consecutive(global_doc_ids, return_counts=True)
147
+ total_doc_tokens = global_doc_ids.shape[0]
148
+
149
+ cu_seqlens = counts.cumsum(0)
150
+ offsets = torch.zeros(counts.shape[0] + 1, dtype=counts.dtype, device=device)
151
+ offsets[1:] = cu_seqlens
152
+ offsets = offsets[:-1]
153
+
154
+ expanded_offsets = torch.repeat_interleave(offsets, counts)
155
+ original_order_ranks = torch.arange(total_doc_tokens, device=device) - expanded_offsets
156
+
157
+ chunk_indices = original_order_ranks // self.pooling_kernel_size
158
+ max_chunks_per_doc = (q_len // self.pooling_kernel_size) + 1
159
+
160
+ global_chunk_ids = global_doc_ids * max_chunks_per_doc + chunk_indices
161
+
162
+ unique_global_chunk_ids, chunk_token_counts = torch.unique_consecutive(global_chunk_ids, return_counts=True)
163
+ pooled_doc_ids = unique_global_chunk_ids // max_chunks_per_doc % (max_doc_id + 1)
164
+
165
+
166
+ pooled_k_chunks, pooled_v_chunks = self.sequence_pooling_kv(
167
+ key_states,
168
+ value_states,
169
+ doc_indices,
170
+ global_chunk_ids,
171
+ )
172
+
173
+ pooled_k_chunks = pooled_k_chunks.transpose(0, 1).unsqueeze(0)
174
+ pooled_v_chunks = pooled_v_chunks.transpose(0, 1).unsqueeze(0)
175
+
176
+ pooled_router_k = None
177
+ if self.decouple_router:
178
+ r_k_raw = self.router_k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
179
+ r_k_docs = r_k_raw[doc_indices[:, 0], :, doc_indices[:, 1]]
180
+
181
+ _, chunk_lengths = torch.unique_consecutive(global_chunk_ids, return_counts=True)
182
+ chunk_counts_view = chunk_lengths.view(-1, 1, 1).to(dtype=torch.float32)
183
+ b_k, h_k, d_k = r_k_docs.shape
184
+ k_flat = r_k_docs.reshape(b_k, -1).to(dtype=torch.float32)
185
+ k_cumsum = F.pad(torch.cumsum(k_flat, dim=0), (0, 0, 1, 0))
186
+ chunk_cu_seqlens = F.pad(torch.cumsum(chunk_lengths, 0), (1, 0))
187
+ k_sums_flat = k_cumsum[chunk_cu_seqlens[1:]] - k_cumsum[chunk_cu_seqlens[:-1]]
188
+ pooled_router_k = (k_sums_flat.view(unique_global_chunk_ids.shape[0], h_k, d_k) / chunk_counts_view).to(dtype=r_k_docs.dtype)
189
+
190
+ pooled_router_k = pooled_router_k.transpose(0, 1).unsqueeze(0)
191
+
192
+ if self.aux_loss_method == "INFONCE":
193
+ router_k = pooled_router_k if pooled_router_k is not None else pooled_k_chunks
194
+ pooled_router_k = F.normalize(router_k, p=2, dim=-1)
195
+
196
+ if past_key_value is not None:
197
+ num_template_mask_prefix = (doc_ids == -2).sum()
198
+ template_prefix_kcache = key_states[:, :, :num_template_mask_prefix]
199
+ template_prefix_vcache = value_states[:, :, :num_template_mask_prefix]
200
+ kwargs = {
201
+ "template_prefix_kcache": template_prefix_kcache,
202
+ "template_prefix_vcache": template_prefix_vcache,
203
+ }
204
+ if self.is_router_layer:
205
+ pooled_k_chunks, pooled_v_chunks = past_key_value.update(pooled_k_chunks, pooled_v_chunks, self.layer_idx)
206
+ kwargs2 = {
207
+ "doc_id_bias": doc_ids.shape[1],
208
+ "pooled_doc_ids": pooled_doc_ids,
209
+ "prefill_stage1_kvcache_size": pooled_k_chunks.shape[2],
210
+ }
211
+ if pooled_router_k is not None:
212
+ past_key_value.update_router_kcache(pooled_router_k, self.layer_idx)
213
+ kwargs.update(kwargs2)
214
+ past_key_value.record_kwargs(self.layer_idx, kwargs)
215
+
216
+ key_states = repeat_kv(key_states, self.num_key_value_groups)
217
+ value_states = repeat_kv(value_states, self.num_key_value_groups)
218
+
219
+ attn_output = torch.zeros((bsz, q_len, self.config.num_attention_heads * self.head_dim), device=device, dtype=dtype)
220
+ indices_b = torch.nonzero(doc_token_mask, as_tuple=False)
221
+
222
+ if indices_b.shape[0] > 0:
223
+ q_b, k_b, v_b = query_states[indices_b[:, 0], :, indices_b[:, 1]], key_states[indices_b[:, 0], :, indices_b[:, 1]], value_states[indices_b[:, 0], :, indices_b[:, 1]]
224
+ doc_ids_b = doc_ids[indices_b[:, 0], indices_b[:, 1]]
225
+ batch_indices_b = indices_b[:, 0]
226
+ global_doc_ids_b = batch_indices_b * (max_doc_id + 1) + doc_ids_b
227
+ _, counts_b = torch.unique_consecutive(global_doc_ids_b, return_counts=True)
228
+ cu_seqlens_b = F.pad(torch.cumsum(counts_b, dim=0, dtype=torch.int32), (1, 0))
229
+ output_b_flat = flash_attn_varlen_func(q_b, k_b, v_b, cu_seqlens_q=cu_seqlens_b, cu_seqlens_k=cu_seqlens_b, max_seqlen_q=int(counts_b.max()), max_seqlen_k=int(counts_b.max()), dropout_p=self.attention_dropout if self.training else 0.0, causal=True).view(-1, self.config.num_attention_heads * self.head_dim)
230
+ attn_output[indices_b[:, 0], indices_b[:, 1]] += output_b_flat
231
+
232
+ template_mask = (doc_ids == -2) & (attention_mask == 1)
233
+ template_indices = torch.nonzero(template_mask, as_tuple=False)
234
+ if template_indices.shape[0] > 0:
235
+ q_template = query_states.transpose(1, 2)[template_mask]
236
+ k_template = key_states.transpose(1, 2)[template_mask]
237
+ v_template = value_states.transpose(1, 2)[template_mask]
238
+ template_counts_per_sample = torch.bincount(template_indices[:, 0], minlength=bsz)
239
+ cu_seqlens_template = F.pad(torch.cumsum(template_counts_per_sample, dim=0, dtype=torch.int32), (1, 0))
240
+ output_template_flat = flash_attn_varlen_func(q_template, k_template, v_template, cu_seqlens_q=cu_seqlens_template, cu_seqlens_k=cu_seqlens_template, max_seqlen_q=int(template_counts_per_sample.max()), max_seqlen_k=int(template_counts_per_sample.max()), dropout_p=0.0, causal=True).view(-1, self.config.num_attention_heads * self.head_dim)
241
+ attn_output[template_mask] = output_template_flat
242
+
243
+ return self.o_proj(attn_output), None
244
+
245
+ elif stage == "prefill_stage2":
246
+ cache_kwargs = past_key_value.cache_kwargs[self.layer_idx]
247
+ if self.memory_client is not None:
248
+ if self.template_prefix_kcache is None:
249
+ self.template_prefix_kcache , self.template_prefix_vcache = self.memory_client.get_template_prefix_kvcaches(self.layer_idx)
250
+ if not self.template_prefix_kcache.is_cuda:
251
+ self.template_prefix_kcache = self.template_prefix_kcache.to(device)
252
+ if not self.template_prefix_vcache.is_cuda:
253
+ self.template_prefix_vcache = self.template_prefix_vcache.to(device)
254
+ template_prefix_kcache = self.template_prefix_kcache
255
+ template_prefix_vcache = self.template_prefix_vcache
256
+ else:
257
+ template_prefix_kcache = cache_kwargs["template_prefix_kcache"].to(device)
258
+ template_prefix_vcache = cache_kwargs["template_prefix_vcache"].to(device)
259
+
260
+ final_k_to_scatter, final_v_to_scatter = None, None
261
+
262
+ if self.is_router_layer:
263
+ routing_q_for_scoring = self.router_q_proj(hidden_states).view(hidden_shape).transpose(1, 2) if self.decouple_router else query_states
264
+ if self.aux_loss_method == "INFONCE":
265
+ routing_q_for_scoring = F.normalize(routing_q_for_scoring, p=2, dim=-1)
266
+
267
+ query_mask = ((doc_ids == 0) & (attention_mask == 1))
268
+ res = self.memory_client.doc_query(routing_q_for_scoring, query_mask, self.layer_idx)
269
+ final_k_to_scatter, final_v_to_scatter, final_scores, num_selected_chunks_per_sample, final_selected_doc_ids = res
270
+
271
+ if past_key_value.meta.get("require_recall_topk", False):
272
+ recall_topk_list = []
273
+ for i in range(bsz):
274
+ recall_topk_list.append({
275
+ "topk_doc_ids": final_selected_doc_ids[i].cpu().detach().tolist(),
276
+ "score": final_scores[i].cpu().detach().tolist(),
277
+ })
278
+ cache_kwargs["recall_topk"] = recall_topk_list
279
+ else:
280
+ num_selected_chunks_per_sample = torch.zeros(bsz, dtype=torch.long, device=device)
281
+
282
+ num_q_per_sample = attention_mask.sum(dim=1)
283
+ template_len = template_prefix_kcache.shape[2]
284
+ kv_lengths = template_len + num_selected_chunks_per_sample + num_q_per_sample
285
+
286
+ cu_seqlens_q = F.pad(num_q_per_sample.cumsum(0, dtype=torch.int32), (1, 0))
287
+ cu_seqlens_kv = F.pad(kv_lengths.cumsum(0, dtype=torch.int32), (1, 0))
288
+
289
+ total_q_tokens = cu_seqlens_q[-1].item()
290
+ total_kv_tokens = cu_seqlens_kv[-1].item()
291
+
292
+ q_final = torch.empty((total_q_tokens, self.config.num_attention_heads, self.head_dim), device=device, dtype=dtype)
293
+ k_final_unrepeated = torch.empty((self.config.num_key_value_heads, total_kv_tokens, self.head_dim), device=device, dtype=dtype)
294
+ v_final_unrepeated = torch.empty((self.config.num_key_value_heads, total_kv_tokens, self.head_dim), device=device, dtype=dtype)
295
+
296
+ valid_q_mask = (attention_mask == 1)
297
+ q_final = query_states.permute(0, 2, 1, 3)[valid_q_mask]
298
+
299
+ offset_start_sample = cu_seqlens_kv[:-1]
300
+ offset_start_template = offset_start_sample
301
+ offset_start_chunks = offset_start_sample + template_len
302
+ offset_start_question = offset_start_chunks + num_selected_chunks_per_sample
303
+
304
+ template_indices = torch.arange(template_len, device=device).unsqueeze(0) + offset_start_template.unsqueeze(1)
305
+ source_k_template = template_prefix_kcache.expand(bsz, -1, -1, -1).permute(1, 0, 2, 3).reshape(self.config.num_key_value_heads, -1, self.head_dim)
306
+ k_final_unrepeated[:, template_indices.flatten(), :] = source_k_template
307
+ source_v_template = template_prefix_vcache.expand(bsz, -1, -1, -1).permute(1, 0, 2, 3).reshape(self.config.num_key_value_heads, -1, self.head_dim)
308
+ v_final_unrepeated[:, template_indices.flatten(), :] = source_v_template
309
+
310
+ if self.is_router_layer and final_k_to_scatter is not None and final_k_to_scatter.shape[1] > 0:
311
+ batch_indices_for_chunks = torch.arange(bsz, device=device).repeat_interleave(num_selected_chunks_per_sample)
312
+ is_start_of_sample = torch.cat([torch.tensor([True], device=device), batch_indices_for_chunks[1:] != batch_indices_for_chunks[:-1]])
313
+ cumsum_ranks = torch.ones_like(batch_indices_for_chunks).cumsum(0)
314
+ start_offsets = cumsum_ranks[is_start_of_sample].repeat_interleave(num_selected_chunks_per_sample)
315
+ chunk_rank_in_sample = cumsum_ranks - start_offsets
316
+
317
+ chunk_dest_indices = offset_start_chunks[batch_indices_for_chunks] + chunk_rank_in_sample
318
+
319
+ k_final_unrepeated[:, chunk_dest_indices, :] = final_k_to_scatter
320
+ if final_v_to_scatter.device == torch.device("cpu"):
321
+ final_v_to_scatter = final_v_to_scatter.to(device)
322
+ v_final_unrepeated[:, chunk_dest_indices, :] = final_v_to_scatter
323
+
324
+ batch_indices_for_q = torch.arange(bsz, device=device).repeat_interleave(num_q_per_sample)
325
+ q_rank_in_sample = (torch.cumsum(valid_q_mask.int(), dim=1) - 1)[valid_q_mask]
326
+ q_dest_indices = offset_start_question[batch_indices_for_q] + q_rank_in_sample
327
+
328
+ k_final_unrepeated[:, q_dest_indices, :] = key_states.permute(1, 0, 2, 3).reshape(self.config.num_key_value_heads, -1, self.head_dim)[:, valid_q_mask.flatten(), :]
329
+ v_final_unrepeated[:, q_dest_indices, :] = value_states.permute(1, 0, 2, 3).reshape(self.config.num_key_value_heads, -1, self.head_dim)[:, valid_q_mask.flatten(), :]
330
+
331
+ k_final = k_final_unrepeated
332
+ v_final = v_final_unrepeated
333
+
334
+ output_flat = flash_attn_varlen_func(
335
+ q=q_final, k=k_final.transpose(0,1), v=v_final.transpose(0,1),
336
+ cu_seqlens_q=cu_seqlens_q, cu_seqlens_k=cu_seqlens_kv,
337
+ max_seqlen_q=num_q_per_sample.max().item(), max_seqlen_k=kv_lengths.max().item(),
338
+ dropout_p=0.0, causal=True
339
+ ).view(-1, self.config.num_attention_heads * self.head_dim)
340
+
341
+ attn_output = torch.zeros((bsz, q_len, self.config.num_attention_heads * self.head_dim), device=device, dtype=dtype)
342
+ attn_output[valid_q_mask] = output_flat
343
+
344
+ max_kv_len = kv_lengths.max().item()
345
+ compacked_key_cache = torch.zeros((bsz, self.config.num_key_value_heads, max_kv_len, self.head_dim), dtype=dtype, device=device)
346
+ compacked_value_cache = torch.zeros((bsz, self.config.num_key_value_heads, max_kv_len, self.head_dim), dtype=dtype, device=device)
347
+
348
+ left_pad_mask = torch.arange(max_kv_len, device=device).unsqueeze(0) >= (max_kv_len - kv_lengths.unsqueeze(1))
349
+
350
+ compacked_key_cache.permute(0, 2, 1, 3)[left_pad_mask] = k_final_unrepeated.permute(1, 0, 2)
351
+ compacked_value_cache.permute(0, 2, 1, 3)[left_pad_mask] = v_final_unrepeated.permute(1, 0, 2)
352
+
353
+ cache_kwargs["compacked_key_cache"] = compacked_key_cache
354
+ cache_kwargs["compacked_value_cache"] = compacked_value_cache
355
+ cache_kwargs["kv_lengths"] = kv_lengths
356
+ cache_kwargs["attention_mask"] = left_pad_mask
357
+ past_key_value.record_kwargs(self.layer_idx, cache_kwargs)
358
+
359
+ return self.o_proj(attn_output), None
360
+
361
+ else:
362
+ cache_kwargs = past_key_value.cache_kwargs[self.layer_idx]
363
+ if "compacked_key_cache" not in cache_kwargs:
364
+ raise ValueError("批次化紧凑KV缓存未找到。Prefill stage 2 是否正确运行?")
365
+
366
+ compacked_key_cache = cache_kwargs["compacked_key_cache"]
367
+ compacked_value_cache = cache_kwargs["compacked_value_cache"]
368
+ kv_lengths = cache_kwargs["kv_lengths"]
369
+ layer_attention_mask = cache_kwargs["attention_mask"]
370
+
371
+ max_kv_len = compacked_key_cache.shape[2]
372
+ full_k_unrepeated = torch.cat([compacked_key_cache, key_states], dim=2)
373
+ full_v_unrepeated = torch.cat([compacked_value_cache, value_states], dim=2)
374
+
375
+ if past_key_value.meta.get("qa_mode", False):
376
+ cur_layer_attention_mask = torch.LongTensor([[1] * q_len for _ in range(bsz)]).to(device)
377
+ cur_layer_attention_mask = (cur_layer_attention_mask * attention_mask).type(layer_attention_mask.dtype)
378
+ layer_attention_mask = torch.cat([layer_attention_mask, cur_layer_attention_mask], dim=1)
379
+ attn_mask_4d = layer_attention_mask[:, None, None, :].expand(-1, self.config.num_attention_heads, 1, -1)
380
+ cache_kwargs["attention_mask"] = layer_attention_mask
381
+ else:
382
+ new_kv_lengths = kv_lengths + 1
383
+ max_new_kv_len = max_kv_len + 1
384
+ attn_mask_2d = torch.arange(max_new_kv_len, device=device).unsqueeze(0) >= (max_new_kv_len - new_kv_lengths.unsqueeze(1))
385
+
386
+ attn_mask_4d = attn_mask_2d[:, None, None, :].expand(-1, self.config.num_attention_heads, 1, -1)
387
+ cache_kwargs["kv_lengths"] = new_kv_lengths
388
+
389
+ key_states_gqa = repeat_kv(full_k_unrepeated, self.num_key_value_groups)
390
+ value_states_gqa = repeat_kv(full_v_unrepeated, self.num_key_value_groups)
391
+
392
+ attn_output = F.scaled_dot_product_attention(
393
+ query_states,
394
+ key_states_gqa,
395
+ value_states_gqa,
396
+ attn_mask=attn_mask_4d,
397
+ dropout_p=0.0,
398
+ is_causal=False
399
+ ).transpose(1, 2).reshape(bsz, q_len, -1)
400
+
401
+ cache_kwargs["compacked_key_cache"] = full_k_unrepeated
402
+ cache_kwargs["compacked_value_cache"] = full_v_unrepeated
403
+ past_key_value.record_kwargs(self.layer_idx, cache_kwargs)
404
+
405
+ return self.o_proj(attn_output), None
406
+
407
+ def _calculate_routing_scores_adaptive(
408
+ self,
409
+ query_states: torch.Tensor, # [B, H, Q_len, D]
410
+ pooled_k_bched: torch.Tensor, # [B, C, H, D]
411
+ routing_query_mask: torch.Tensor, # [B, Q_len] - 1 for valid, 0 for pad
412
+ chunk_mask: torch.Tensor, # [B, C] - 1 for valid, 0 for pad
413
+ ) -> torch.Tensor:
414
+ bsz, num_heads, q_len, head_dim = query_states.shape
415
+ _, max_chunks, _, _ = pooled_k_bched.shape
416
+ dtype, device = query_states.dtype, query_states.device
417
+ min_val = torch.finfo(dtype).min
418
+
419
+ k_states_T = pooled_k_bched.permute(0, 2, 3, 1)
420
+
421
+ current_scaling = 1.0 if self.decouple_router and "INFONCE" in self.aux_loss_method else self.scaling
422
+ scores = torch.matmul(query_states, k_states_T) * current_scaling
423
+
424
+ q_mask_expanded = routing_query_mask.view(bsz, 1, q_len, 1)
425
+ k_mask_expanded = chunk_mask.view(bsz, 1, 1, max_chunks)
426
+
427
+ final_mask = q_mask_expanded & k_mask_expanded
428
+ scores.masked_fill_(~final_mask, min_val)
429
+
430
+ if self.head_reduce_method == "max":
431
+ scores = scores.max(dim=1).values
432
+ elif self.head_reduce_method == "mean":
433
+ scores = scores.mean(dim=1)
434
+ else:
435
+ raise NotImplementedError(f"Unsupported head reduce method: {self.head_reduce_method}")
436
+
437
+ if self.query_reduce_method == "max":
438
+ scores_final = scores.max(dim=1).values
439
+
440
+ elif self.query_reduce_method == "mean":
441
+ valid_mask = final_mask.squeeze(1) # [B, Q_len, C]
442
+
443
+ scores_clean = torch.where(valid_mask, scores, torch.zeros_like(scores))
444
+ sum_scores = scores_clean.sum(dim=1) # [B, C]
445
+ counts = valid_mask.sum(dim=1).to(dtype).clamp(min=1.0)
446
+ mean_scores = sum_scores / counts
447
+
448
+ scores_final = torch.where(
449
+ chunk_mask,
450
+ mean_scores,
451
+ torch.tensor(min_val, device=device, dtype=dtype)
452
+ )
453
+
454
+ elif self.query_reduce_method == "last":
455
+ q_lens = routing_query_mask.sum(dim=1).long()
456
+ last_indices = (q_lens - 1).clamp(min=0)
457
+
458
+ gather_idx = last_indices.view(bsz, 1, 1).expand(-1, 1, max_chunks)
459
+ scores_final = scores.gather(1, gather_idx).squeeze(1)
460
+ scores_final.masked_fill_(~chunk_mask, min_val)
461
+
462
+ else:
463
+ raise NotImplementedError(f"Unsupported query reduce method: {self.query_reduce_method}")
464
+
465
+ return scores_final
466
+
467
+ def sequence_pooling_kv(self, key_states, value_states, doc_indices, global_chunk_ids):
468
+ k_docs = key_states[doc_indices[:, 0], :, doc_indices[:, 1]]
469
+ v_docs = value_states[doc_indices[:, 0], :, doc_indices[:, 1]]
470
+ unique_global_chunk_ids, chunk_lengths = torch.unique_consecutive(global_chunk_ids, return_counts=True)
471
+
472
+ num_unique_chunks = unique_global_chunk_ids.shape[0]
473
+ chunk_counts_view = chunk_lengths.view(-1, 1, 1).to(dtype=torch.float32)
474
+
475
+ def compute_pooled_states_via_cumsum(states, counts_view, lengths):
476
+ b, h, d = states.shape
477
+ states_flat = states.reshape(b, -1).to(dtype=torch.float32)
478
+ states_cumsum = F.pad(torch.cumsum(states_flat, dim=0), (0, 0, 1, 0))
479
+ chunk_cu_seqlens = F.pad(torch.cumsum(lengths, 0), (1, 0))
480
+ state_sums_flat = states_cumsum[chunk_cu_seqlens[1:]] - states_cumsum[chunk_cu_seqlens[:-1]]
481
+ state_sums = state_sums_flat.view(num_unique_chunks, h, d)
482
+ return (state_sums / counts_view).to(dtype=states.dtype)
483
+
484
+ pooled_k_chunks = compute_pooled_states_via_cumsum(k_docs, chunk_counts_view, chunk_lengths)
485
+ pooled_v_chunks = compute_pooled_states_via_cumsum(v_docs, chunk_counts_view, chunk_lengths)
486
+ return pooled_k_chunks, pooled_v_chunks
487
+
488
+ def sequence_pooling_qkv(self, query_states, key_states, value_states, doc_indices, global_chunk_ids):
489
+ q_docs = query_states[doc_indices[:, 0], :, doc_indices[:, 1]]
490
+ k_docs = key_states[doc_indices[:, 0], :, doc_indices[:, 1]]
491
+ v_docs = value_states[doc_indices[:, 0], :, doc_indices[:, 1]]
492
+ unique_global_chunk_ids, chunk_lengths = torch.unique_consecutive(global_chunk_ids, return_counts=True)
493
+
494
+ num_unique_chunks = unique_global_chunk_ids.shape[0]
495
+ chunk_counts_view = chunk_lengths.view(-1, 1, 1).to(dtype=torch.float32)
496
+ def compute_pooled_states_via_cumsum(states, counts_view, lengths):
497
+ b, h, d = states.shape
498
+ states_flat = states.reshape(b, -1).to(dtype=torch.float32)
499
+ states_cumsum = F.pad(torch.cumsum(states_flat, dim=0), (0, 0, 1, 0))
500
+ chunk_cu_seqlens = F.pad(torch.cumsum(lengths, 0), (1, 0))
501
+
502
+ state_sums_flat = states_cumsum[chunk_cu_seqlens[1:]] - states_cumsum[chunk_cu_seqlens[:-1]]
503
+ state_sums = state_sums_flat.view(num_unique_chunks, h, d)
504
+ return (state_sums / counts_view).to(dtype=states.dtype)
505
+
506
+ pooled_q_chunks = compute_pooled_states_via_cumsum(q_docs, chunk_counts_view, chunk_lengths)
507
+ pooled_k_chunks = compute_pooled_states_via_cumsum(k_docs, chunk_counts_view, chunk_lengths)
508
+ pooled_v_chunks = compute_pooled_states_via_cumsum(v_docs, chunk_counts_view, chunk_lengths)
509
+ return pooled_q_chunks, pooled_k_chunks, pooled_v_chunks
510
+
511
+ def count_chunks_per_batch(self, doc_ids, attention_mask, kernel_size):
512
+ batch_size = doc_ids.size(0)
513
+ chunk_counts = []
514
+
515
+ for i in range(batch_size):
516
+ mask = attention_mask[i]
517
+ ids = doc_ids[i]
518
+ valid_ids = ids[mask == 1]
519
+
520
+ if len(valid_ids) == 0:
521
+ chunk_counts.append(0)
522
+ continue
523
+ _, counts = torch.unique_consecutive(valid_ids, return_counts=True)
524
+
525
+ num_chunks = (counts + kernel_size - 1) // kernel_size
526
+ total_chunks = num_chunks.sum().item()
527
+ chunk_counts.append(total_chunks)
528
+
529
+ return torch.LongTensor(chunk_counts).to(doc_ids.device)
530
+
531
+ def _forward(
532
+ self,
533
+ hidden_states: torch.Tensor,
534
+ doc_ids: torch.LongTensor,
535
+ attention_mask: Optional[torch.Tensor] = None,
536
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
537
+ past_key_value: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
538
+ **kwargs,
539
+ ) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
540
+ bsz, q_len, _ = hidden_states.shape
541
+ device, dtype = hidden_states.device, hidden_states.dtype
542
+ hidden_shape = (bsz, q_len, -1, self.head_dim)
543
+
544
+ query_states = self.q_norm(self.q_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
545
+ key_states = self.k_norm(self.k_proj(hidden_states).view(hidden_shape)).transpose(1, 2)
546
+ value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
547
+ cos, sin = position_embeddings
548
+ query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
549
+
550
+ key_states = repeat_kv(key_states, self.num_key_value_groups)
551
+ value_states = repeat_kv(value_states, self.num_key_value_groups)
552
+
553
+ routing_query_mask = (doc_ids == 0) & (attention_mask == 1)
554
+ doc_token_mask = (doc_ids > 0) & (attention_mask == 1)
555
+
556
+ query_indices = torch.nonzero(routing_query_mask, as_tuple=False)
557
+ doc_indices = torch.nonzero(doc_token_mask, as_tuple=False)
558
+
559
+ if doc_indices.shape[0] == 0 or query_indices.shape[0] == 0:
560
+ raise ValueError("No query or doc tokens found")
561
+
562
+ max_doc_id = int(doc_ids.max().item())
563
+ attn_output = torch.zeros((bsz, q_len, self.config.num_attention_heads * self.head_dim), device=device, dtype=dtype)
564
+ if self.is_router_layer:
565
+ original_doc_ids = doc_ids[doc_token_mask]
566
+ original_doc_batch_indices = doc_indices[:, 0]
567
+
568
+ global_doc_ids = original_doc_batch_indices * (max_doc_id + 1) + original_doc_ids
569
+ _, counts = torch.unique_consecutive(global_doc_ids, return_counts=True)
570
+ total_doc_tokens = global_doc_ids.shape[0]
571
+
572
+ offsets = torch.zeros(counts.shape[0] + 1, dtype=counts.dtype, device=device)
573
+ offsets[1:] = counts.cumsum(0)
574
+ offsets = offsets[:-1]
575
+
576
+ expanded_offsets = torch.repeat_interleave(offsets, counts)
577
+ original_order_ranks = torch.arange(total_doc_tokens, device=device) - expanded_offsets
578
+
579
+ chunk_indices = original_order_ranks // self.pooling_kernel_size
580
+ max_chunks_per_doc = (q_len // self.pooling_kernel_size) + 1
581
+
582
+ global_chunk_ids = global_doc_ids * max_chunks_per_doc + chunk_indices
583
+
584
+ routing_q_states = None
585
+ routing_pooled_k_chunks = None
586
+
587
+ if self.decouple_router:
588
+ routing_q_states = self.router_q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
589
+ if "INFONCE" in self.aux_loss_method:
590
+ routing_q_states = F.normalize(routing_q_states, p=2, dim=-1)
591
+
592
+
593
+ r_k_raw = self.router_k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
594
+ r_k_raw = repeat_kv(r_k_raw, self.num_key_value_groups)
595
+ r_k_docs = r_k_raw[doc_indices[:, 0], :, doc_indices[:, 1]]
596
+
597
+ unique_global_chunk_ids = torch.unique_consecutive(global_chunk_ids)
598
+ _, chunk_lengths = torch.unique_consecutive(global_chunk_ids, return_counts=True)
599
+
600
+ chunk_counts_view = chunk_lengths.view(-1, 1, 1).to(dtype=torch.float32)
601
+ b_k, h_k, d_k = r_k_docs.shape
602
+ k_flat = r_k_docs.reshape(b_k, -1).to(dtype=torch.float32)
603
+ k_cumsum = F.pad(torch.cumsum(k_flat, dim=0), (0, 0, 1, 0))
604
+ chunk_cu_seqlens = F.pad(torch.cumsum(chunk_lengths, 0), (1, 0))
605
+ k_sums_flat = k_cumsum[chunk_cu_seqlens[1:]] - k_cumsum[chunk_cu_seqlens[:-1]]
606
+ routing_pooled_k_chunks = (k_sums_flat.view(unique_global_chunk_ids.shape[0], h_k, d_k) / chunk_counts_view).to(dtype=r_k_docs.dtype)
607
+ if "INFONCE" in self.aux_loss_method:
608
+ routing_pooled_k_chunks = F.normalize(routing_pooled_k_chunks, p=2, dim=-1)
609
+
610
+ pooled_q_chunks = query_states[doc_indices[:, 0], :, doc_indices[:, 1]]
611
+ pooled_k_chunks = key_states[doc_indices[:, 0], :, doc_indices[:, 1]]
612
+ pooled_v_chunks = value_states[doc_indices[:, 0], :, doc_indices[:, 1]]
613
+ num_doc_tokens = pooled_q_chunks.shape[0]
614
+ num_chunks = num_doc_tokens // self.pooling_kernel_size
615
+
616
+ pooled_q_chunks = pooled_q_chunks.view(num_chunks, self.pooling_kernel_size, self.num_heads, self.head_dim).mean(dim=1)
617
+ pooled_k_chunks = pooled_k_chunks.view(num_chunks, self.pooling_kernel_size, self.num_heads, self.head_dim).mean(dim=1)
618
+ pooled_v_chunks = pooled_v_chunks.view(num_chunks, self.pooling_kernel_size, self.num_heads, self.head_dim).mean(dim=1)
619
+ num_heads = self.config.num_attention_heads
620
+ head_dim = self.head_dim
621
+ else:
622
+ pooled_q_chunks, pooled_k_chunks, pooled_v_chunks = self.sequence_pooling_qkv(
623
+ query_states,
624
+ key_states,
625
+ value_states,
626
+ doc_indices,
627
+ global_chunk_ids,
628
+ )
629
+ num_heads = self.config.num_attention_heads
630
+ head_dim = self.head_dim
631
+
632
+ routing_q_states = query_states
633
+ routing_pooled_k_chunks = pooled_k_chunks
634
+ if "INFONCE" in self.aux_loss_method:
635
+ routing_q_states = F.normalize(routing_q_states, p=2, dim=-1)
636
+ routing_pooled_k_chunks = F.normalize(routing_pooled_k_chunks, p=2, dim=-1)
637
+
638
+ unique_global_chunk_ids = torch.unique_consecutive(global_chunk_ids)
639
+ num_unique_chunks = unique_global_chunk_ids.shape[0]
640
+ chunks_per_sample = self.count_chunks_per_batch(doc_ids, doc_token_mask, kernel_size=self.pooling_kernel_size)
641
+
642
+ max_chunks = chunks_per_sample.max().item()
643
+ pooled_router_k_bched = torch.zeros((bsz, max_chunks, num_heads, self.head_dim), device=device, dtype=dtype)
644
+ chunk_mask = torch.arange(max_chunks, device=device).unsqueeze(0) < chunks_per_sample.unsqueeze(1)
645
+ pooled_router_k_bched[chunk_mask] = routing_pooled_k_chunks
646
+ q_lens = routing_query_mask.sum(dim=1) # (B,)
647
+ max_q_len = int(q_lens.max().item())
648
+
649
+ if max_q_len == 0:
650
+ max_q_len = 1
651
+ valid_q_flat = routing_q_states.transpose(1, 2)[routing_query_mask] # [Total_Valid_Q, H, D]
652
+
653
+ compact_q_states_t = torch.zeros(
654
+ bsz, max_q_len, self.config.num_attention_heads, self.head_dim,
655
+ device=device, dtype=dtype
656
+ )
657
+
658
+ idx_range = torch.arange(max_q_len, device=device).unsqueeze(0)
659
+ mask_compact_q = idx_range < q_lens.unsqueeze(1)
660
+
661
+ compact_q_states_t[mask_compact_q] = valid_q_flat
662
+ compact_q_states = compact_q_states_t.transpose(1, 2)
663
+
664
+ max_scores_per_chunk = self._calculate_routing_scores_adaptive(
665
+ compact_q_states, # (B, H, S, D)
666
+ pooled_router_k_bched, # (B, C, H, D)
667
+ mask_compact_q, # (B, S)
668
+ chunk_mask # (B, C)
669
+ )
670
+
671
+ pooled_global_doc_ids = unique_global_chunk_ids // max_chunks_per_doc
672
+ pooled_doc_ids_in_sample = pooled_global_doc_ids % (max_doc_id + 1)
673
+ chunk_to_doc_id_flat = pooled_doc_ids_in_sample # 形状: (total_chunks, )
674
+
675
+ chunk_to_doc_id_bched = torch.full((bsz, max_chunks), 0, dtype=torch.long, device=device)
676
+ chunk_to_doc_id_bched[chunk_mask] = chunk_to_doc_id_flat
677
+
678
+ offsets = torch.arange(bsz, device=device) * (max_doc_id + 1)
679
+ global_chunk_to_doc_id = chunk_to_doc_id_bched + offsets.unsqueeze(1)
680
+ flat_doc_scores = torch.full((bsz * (max_doc_id + 1),), -float('inf'), device=device, dtype=dtype)
681
+
682
+ valid_scores_flat = max_scores_per_chunk[chunk_mask]
683
+ valid_global_doc_ids_flat = global_chunk_to_doc_id[chunk_mask]
684
+
685
+ if self.chunk_reduce_method == "max":
686
+ doc_scores = flat_doc_scores.scatter_reduce(
687
+ dim=0,
688
+ index=valid_global_doc_ids_flat,
689
+ src=valid_scores_flat,
690
+ reduce="amax",
691
+ include_self=True
692
+ )
693
+
694
+ elif self.chunk_reduce_method == "mean":
695
+ flat_doc_sums = torch.zeros_like(flat_doc_scores)
696
+
697
+ flat_doc_sums = flat_doc_sums.scatter_reduce(
698
+ dim=0,
699
+ index=valid_global_doc_ids_flat,
700
+ src=valid_scores_flat,
701
+ reduce="sum",
702
+ include_self=False
703
+ )
704
+
705
+ flat_doc_counts = torch.zeros_like(flat_doc_scores)
706
+ ones = torch.ones_like(valid_scores_flat)
707
+
708
+ flat_doc_counts = flat_doc_counts.scatter_reduce(
709
+ dim=0,
710
+ index=valid_global_doc_ids_flat,
711
+ src=ones,
712
+ reduce="sum",
713
+ include_self=False
714
+ )
715
+
716
+ flat_doc_counts_safe = flat_doc_counts.clamp(min=1.0)
717
+ mean_scores = flat_doc_sums / flat_doc_counts_safe
718
+
719
+ doc_scores = torch.where(
720
+ flat_doc_counts > 0,
721
+ mean_scores,
722
+ flat_doc_scores # 这里是 -inf
723
+ )
724
+
725
+ else:
726
+ raise ValueError(f"Invalid chunk reduction method: {self.chunk_reduce_method}")
727
+
728
+ scores_by_batch = doc_scores.view(bsz, -1)
729
+ return_scores_by_batch = scores_by_batch.clone()
730
+
731
+ num_docs_per_sample = (scores_by_batch > -1e9).sum(dim=1)
732
+ # 为每个样本计算k值:取配置的top_k和实际文档数的较小者
733
+ k_per_sample = torch.min(num_docs_per_sample, torch.full_like(num_docs_per_sample, self.top_k_docs))
734
+
735
+ _, sorted_indices = torch.sort(scores_by_batch, dim=1, descending=True)
736
+
737
+ range_tensor = torch.arange(scores_by_batch.shape[1], device=device).expand(bsz, -1)
738
+ selection_mask = range_tensor < k_per_sample.unsqueeze(1)
739
+
740
+ selected_docs_indices = sorted_indices.masked_fill(~selection_mask, -50)
741
+
742
+ prompt_and_response_mask = (doc_ids < 1) & (attention_mask == 1)
743
+ # 此处的 selected_docs_indices 已经是修复后的张量,所以这行代码无需修改
744
+ selected_docs_mask = torch.any(doc_ids.unsqueeze(-1) == selected_docs_indices.unsqueeze(1), dim=-1) & doc_token_mask
745
+
746
+ pa_indices = torch.nonzero(prompt_and_response_mask, as_tuple=False)
747
+ q_pa_flat = query_states[pa_indices[:, 0], :, pa_indices[:, 1]]
748
+ k_pa_flat = key_states[pa_indices[:, 0], :, pa_indices[:, 1]]
749
+ v_pa_flat = value_states[pa_indices[:, 0], :, pa_indices[:, 1]]
750
+ sort_key_pa = pa_indices[:, 0] * q_len + pa_indices[:, 1]
751
+
752
+ selected_doc_token_indices = torch.nonzero(selected_docs_mask, as_tuple=False)
753
+ is_doc_token_mask_flat = doc_token_mask.flatten()
754
+ global_chunk_ids_padded = torch.full((bsz * q_len,), -1, dtype=torch.long, device=device)
755
+ global_chunk_ids_padded[is_doc_token_mask_flat] = global_chunk_ids
756
+ selected_chunk_ids_flat = global_chunk_ids_padded.view(bsz, q_len)[selected_docs_mask]
757
+
758
+ unique_selected_chunk_ids, inverse_indices_fix = torch.unique(selected_chunk_ids_flat, sorted=True, return_inverse=True)
759
+ if unique_selected_chunk_ids.numel() > 0:
760
+ first_occurrence_indices = torch.empty_like(unique_selected_chunk_ids, dtype=torch.long)
761
+ first_occurrence_indices.scatter_reduce_(src=torch.arange(selected_chunk_ids_flat.numel(), device=device),index=inverse_indices_fix, dim=0, reduce='amin', include_self=False)
762
+
763
+ representative_indices = selected_doc_token_indices[first_occurrence_indices]
764
+ sort_key_chunks = representative_indices[:, 0] * q_len + representative_indices[:, 1]
765
+
766
+ map_gcid_to_poolidx = torch.full((int(global_chunk_ids.max().item()) + 1,), -1, dtype=torch.long, device=device)
767
+ map_gcid_to_poolidx[unique_global_chunk_ids] = torch.arange(num_unique_chunks, device=device)
768
+
769
+ pool_indices_to_gather = map_gcid_to_poolidx[unique_selected_chunk_ids]
770
+
771
+ assert (pool_indices_to_gather.sort().values != pool_indices_to_gather).sum() == 0
772
+ q_pooled_sel_flat = pooled_q_chunks[pool_indices_to_gather]
773
+ k_pooled_sel_flat = pooled_k_chunks[pool_indices_to_gather]
774
+ v_pooled_sel_flat = pooled_v_chunks[pool_indices_to_gather]
775
+
776
+ batch_indices_chunks = representative_indices[:, 0]
777
+ else:
778
+ sort_key_chunks = torch.tensor([], dtype=torch.long, device=device)
779
+ q_pooled_sel_flat = torch.tensor([], dtype=dtype, device=device).view(0, num_heads, head_dim)
780
+ k_pooled_sel_flat = torch.tensor([], dtype=dtype, device=device).view(0, num_heads, head_dim)
781
+ v_pooled_sel_flat = torch.tensor([], dtype=dtype, device=device).view(0, num_heads, head_dim)
782
+ batch_indices_chunks = torch.tensor([], dtype=torch.long, device=device)
783
+
784
+ q_combined = torch.cat([q_pa_flat, q_pooled_sel_flat], dim=0)
785
+ k_combined = torch.cat([k_pa_flat, k_pooled_sel_flat], dim=0)
786
+ v_combined = torch.cat([v_pa_flat, v_pooled_sel_flat], dim=0)
787
+
788
+ combined_sort_keys = torch.cat([sort_key_pa, sort_key_chunks], dim=0)
789
+ _, final_sort_indices = torch.sort(combined_sort_keys)
790
+
791
+ q_a_final = q_combined[final_sort_indices]
792
+ k_a_final = k_combined[final_sort_indices]
793
+ v_a_final = v_combined[final_sort_indices]
794
+
795
+ # 4.4 计算cu_seqlens (逻辑不变)
796
+ batch_indices_pa = pa_indices[:, 0]
797
+ batch_indices_combined = torch.cat([batch_indices_pa, batch_indices_chunks], dim=0)
798
+ sorted_batch_indices = batch_indices_combined[final_sort_indices]
799
+
800
+ batch_counts_a = torch.bincount(sorted_batch_indices, minlength=bsz)
801
+ cu_seqlens_a = F.pad(torch.cumsum(batch_counts_a, dim=0, dtype=torch.int32), (1, 0))
802
+ else:
803
+ prompt_and_response_mask = (doc_ids < 1) & (attention_mask == 1)
804
+ pa_indices = torch.nonzero(prompt_and_response_mask, as_tuple=False)
805
+ q_a_final = query_states[pa_indices[:, 0], :, pa_indices[:, 1]]
806
+ k_a_final = key_states[pa_indices[:, 0], :, pa_indices[:, 1]]
807
+ v_a_final = value_states[pa_indices[:, 0], :, pa_indices[:, 1]]
808
+ batch_counts_a = prompt_and_response_mask.sum(dim=1)
809
+ cu_seqlens_a = F.pad(torch.cumsum(batch_counts_a, dim=0, dtype=torch.int32), (1, 0))
810
+ return_scores_by_batch = None
811
+
812
+ if q_a_final.shape[0] > 0:
813
+ output_a_final = flash_attn_varlen_func(
814
+ q_a_final, k_a_final, v_a_final,
815
+ cu_seqlens_q=cu_seqlens_a, cu_seqlens_k=cu_seqlens_a,
816
+ max_seqlen_q=int(batch_counts_a.max()), max_seqlen_k=int(batch_counts_a.max()),
817
+ dropout_p=self.attention_dropout if self.training else 0.0,
818
+ causal=True
819
+ ).view(-1, self.config.num_attention_heads * self.head_dim)
820
+
821
+ if self.is_router_layer:
822
+ is_pa_mask_combined = torch.cat([
823
+ torch.ones(pa_indices.shape[0], dtype=torch.bool, device=device),
824
+ torch.zeros(q_pooled_sel_flat.shape[0], dtype=torch.bool, device=device) # 修正为使用池化块的数量
825
+ ], dim=0)
826
+ is_pa_mask_sorted = is_pa_mask_combined[final_sort_indices]
827
+
828
+ output_pa_part = output_a_final[is_pa_mask_sorted]
829
+ attn_output[pa_indices[:, 0], pa_indices[:, 1]] = output_pa_part
830
+ else:
831
+ attn_output[pa_indices[:, 0], pa_indices[:, 1]] = output_a_final
832
+
833
+ indices_b = torch.nonzero(doc_token_mask, as_tuple=False)
834
+ if indices_b.shape[0] > 0:
835
+ q_b, k_b, v_b = query_states[indices_b[:, 0], :, indices_b[:, 1]], key_states[indices_b[:, 0], :, indices_b[:, 1]], value_states[indices_b[:, 0], :, indices_b[:, 1]]
836
+ doc_ids_b = doc_ids[indices_b[:, 0], indices_b[:, 1]]
837
+ batch_indices_b = indices_b[:, 0]
838
+
839
+ global_doc_ids_b = batch_indices_b * (max_doc_id + 1) + doc_ids_b
840
+
841
+ _, counts_b = torch.unique_consecutive(global_doc_ids_b, return_counts=True)
842
+ cu_seqlens_b = F.pad(torch.cumsum(counts_b, dim=0, dtype=torch.int32), (1, 0))
843
+
844
+ output_b_flat = flash_attn_varlen_func(
845
+ q_b, k_b, v_b, cu_seqlens_q=cu_seqlens_b, cu_seqlens_k=cu_seqlens_b,
846
+ max_seqlen_q=int(counts_b.max()), max_seqlen_k=int(counts_b.max()),
847
+ dropout_p=self.attention_dropout if self.training else 0.0, causal=True
848
+ ).view(-1, self.config.num_attention_heads * self.head_dim)
849
+
850
+ attn_output[indices_b[:, 0], indices_b[:, 1]] += output_b_flat
851
+
852
+ return (self.o_proj(attn_output), return_scores_by_batch), None
src/msa/model.py ADDED
@@ -0,0 +1,733 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ sys.path.append(os.getcwd())
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from typing import Union, Optional, Tuple, List, Dict
8
+ from functools import partial
9
+ from dataclasses import dataclass
10
+ from transformers.modeling_outputs import ModelOutput
11
+ from transformers.models.qwen3.modeling_qwen3 import (
12
+ Qwen3Model,
13
+ FlashAttentionKwargs,
14
+ Qwen3Config,
15
+ Qwen3RMSNorm,
16
+ Qwen3RotaryEmbedding,
17
+ DynamicCache,
18
+ Qwen3DecoderLayer,
19
+ Qwen3MLP,
20
+ Qwen3Attention,
21
+ Qwen3PreTrainedModel,
22
+ )
23
+ from transformers.modeling_outputs import CausalLMOutputWithPast, BaseModelOutputWithPast
24
+ from transformers.processing_utils import Unpack
25
+ from transformers.cache_utils import Cache
26
+ from liger_kernel.transformers.model.loss_utils import LigerForCausalLMLoss
27
+ from src.msa import MemorySparseAttention, MSAGenerationMixin, MSAConfig
28
+
29
+ @dataclass
30
+ class MSALayerModelOutputWithPast(ModelOutput):
31
+ last_hidden_state: Optional[torch.FloatTensor] = None
32
+ past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None
33
+ hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
34
+ attentions: Optional[Tuple[torch.FloatTensor, ...]] = None
35
+ all_docs_scores: Optional[Dict] = None
36
+
37
+ @dataclass
38
+ class MSACausalLMOutputWithPast(ModelOutput):
39
+ loss: Optional[torch.FloatTensor] = None
40
+ lm_loss: Optional[torch.FloatTensor] = None
41
+ aux_loss: Optional[torch.FloatTensor] = None
42
+ answer_loss: Optional[torch.FloatTensor] = None
43
+ reconstruction_loss: Optional[torch.FloatTensor] = None
44
+ logits: Optional[torch.FloatTensor] = None
45
+ past_key_values: Optional[Tuple[Tuple[torch.FloatTensor]]] = None
46
+ hidden_states: Optional[Tuple[torch.FloatTensor, ...]] = None
47
+ attentions: Optional[Tuple[torch.FloatTensor, ...]] = None
48
+ temperature: Optional[torch.FloatTensor] = None
49
+ train_router_metrics: Optional[Dict] = None
50
+
51
+ class MSADeocoderLayer(Qwen3DecoderLayer):
52
+ def __init__(self, config: Qwen3Config, layer_idx: int, attn_type: str = "sparse_attention"):
53
+ super().__init__(config=config, layer_idx=layer_idx)
54
+ self.layer_idx = layer_idx
55
+ self.attn_type = attn_type
56
+ self.hidden_size = config.hidden_size
57
+ if attn_type == "full_attention":
58
+ self.self_attn = Qwen3Attention(config=config, layer_idx=layer_idx)
59
+ elif attn_type == "sparse_attention":
60
+ self.self_attn = MemorySparseAttention(config=config, layer_idx=layer_idx)
61
+
62
+ self.mlp = Qwen3MLP(config)
63
+ self.input_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
64
+ self.post_attention_layernorm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
65
+ config.sliding_window = False
66
+
67
+ def forward(
68
+ self,
69
+ hidden_states: torch.Tensor,
70
+ attention_mask: Optional[torch.Tensor] = None,
71
+ position_ids: Optional[torch.LongTensor] = None,
72
+ past_key_value: Optional[Cache] = None,
73
+ output_attentions: Optional[bool] = False,
74
+ output_docs_score: Optional[bool] = False,
75
+ use_cache: Optional[bool] = False,
76
+ cache_position: Optional[torch.LongTensor] = None,
77
+ position_embeddings: Optional[Tuple[torch.Tensor, torch.Tensor]] = None, # necessary, but kept here for BC
78
+ doc_ids: Optional[torch.Tensor] = None,
79
+ input_ids: Optional[torch.LongTensor] = None,
80
+ **kwargs: Unpack[FlashAttentionKwargs],
81
+ ) -> Tuple[torch.FloatTensor, Optional[Tuple[torch.FloatTensor, torch.FloatTensor]]]:
82
+ residual = hidden_states
83
+
84
+ hidden_states = self.input_layernorm(hidden_states)
85
+
86
+ # Self Attention
87
+ if self.attn_type == "full_attention":
88
+ hidden_states, self_attn_weights = self.self_attn(
89
+ hidden_states=hidden_states,
90
+ attention_mask=attention_mask,
91
+ position_ids=position_ids,
92
+ past_key_value=past_key_value,
93
+ output_attentions=output_attentions,
94
+ use_cache=use_cache,
95
+ cache_position=cache_position,
96
+ position_embeddings=position_embeddings,
97
+ doc_ids=doc_ids,
98
+ input_ids=input_ids,
99
+ **kwargs,
100
+ )
101
+ else:
102
+ hidden_states, self_attn_weights = self.self_attn(
103
+ hidden_states=hidden_states,
104
+ attention_mask=attention_mask,
105
+ position_ids=position_ids,
106
+ past_key_value=past_key_value,
107
+ output_attentions=output_attentions,
108
+ use_cache=use_cache,
109
+ cache_position=cache_position,
110
+ position_embeddings=position_embeddings,
111
+ doc_ids=doc_ids,
112
+ input_ids=input_ids,
113
+ **kwargs,
114
+ )
115
+
116
+ if isinstance(hidden_states, tuple):
117
+ hidden_states, docs_score = hidden_states
118
+ else:
119
+ docs_score = None
120
+ hidden_states = residual + hidden_states
121
+
122
+ # Fully Connected
123
+ residual = hidden_states
124
+ hidden_states = self.post_attention_layernorm(hidden_states)
125
+ hidden_states = self.mlp(hidden_states)
126
+ hidden_states = residual + hidden_states
127
+
128
+ outputs = (hidden_states,)
129
+ if output_attentions:
130
+ outputs += (self_attn_weights,)
131
+
132
+ if output_docs_score:
133
+ outputs += (docs_score,)
134
+
135
+ return outputs
136
+
137
+ class MSAModel(Qwen3Model):
138
+ def __init__(self, config: Qwen3Config):
139
+ super().__init__(config)
140
+ self.padding_idx = config.pad_token_id
141
+ self.vocab_size = config.vocab_size
142
+
143
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
144
+ self.rewrite_position = config.msa_config.rewrite_position
145
+ self.layers = nn.ModuleList([
146
+ MSADeocoderLayer(config, layer_idx, attn_type="sparse_attention")
147
+ for layer_idx in range(config.num_hidden_layers)
148
+ ])
149
+ self.norm = Qwen3RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
150
+ self.rotary_emb = Qwen3RotaryEmbedding(config=config)
151
+ self.gradient_checkpointing = False
152
+
153
+ # Initialize weights and apply final processing
154
+ self.post_init()
155
+
156
+ def forward(
157
+ self,
158
+ input_ids: Optional[torch.LongTensor] = None,
159
+ attention_mask: Optional[torch.Tensor] = None,
160
+ position_ids: Optional[torch.LongTensor] = None,
161
+ past_key_values: Optional[Cache] = None,
162
+ inputs_embeds: Optional[torch.FloatTensor] = None,
163
+ use_cache: Optional[bool] = None,
164
+ output_attentions: Optional[bool] = None,
165
+ output_hidden_states: Optional[bool] = None,
166
+ output_docs_score: Optional[bool] = None,
167
+ cache_position: Optional[torch.LongTensor] = None,
168
+ doc_ids: Optional[torch.LongTensor] = None,
169
+ **flash_attn_kwargs: Unpack[FlashAttentionKwargs],
170
+ ) -> BaseModelOutputWithPast:
171
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
172
+ output_hidden_states = (
173
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
174
+ )
175
+
176
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
177
+
178
+ if (input_ids is None) ^ (inputs_embeds is not None):
179
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
180
+
181
+ if self.gradient_checkpointing and self.training and use_cache:
182
+ use_cache = False
183
+
184
+ # TODO (joao): remove this exception in v4.56 -- it exists for users that try to pass a legacy cache
185
+ if not isinstance(past_key_values, (type(None), Cache)):
186
+ raise ValueError("The `past_key_values` should be either a `Cache` object or `None`.")
187
+
188
+ if inputs_embeds is None:
189
+ inputs_embeds = self.embed_tokens(input_ids)
190
+
191
+ if use_cache and past_key_values is None:
192
+ past_key_values = DynamicCache()
193
+
194
+ if cache_position is None:
195
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
196
+ cache_position = torch.arange(
197
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
198
+ )
199
+
200
+ if not self.rewrite_position and self.training:
201
+ position_ids = None
202
+ if position_ids is None:
203
+ position_ids = cache_position.unsqueeze(0)
204
+
205
+ # causal_mask = self._update_causal_mask(
206
+ # attention_mask, inputs_embeds, cache_position, past_key_values, output_attentions
207
+ # )
208
+ causal_mask = attention_mask
209
+
210
+ hidden_states = inputs_embeds
211
+ # import pdb;pdb.set_trace()
212
+ # create position embeddings to be shared across the decoder layers
213
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
214
+
215
+ # decoder layers
216
+ all_hidden_states = () if output_hidden_states else None
217
+ all_self_attns = () if output_attentions else None
218
+ all_docs_scores = () if output_docs_score else None
219
+
220
+ for decoder_layer in self.layers[: self.config.num_hidden_layers]:
221
+ if output_hidden_states:
222
+ all_hidden_states += (hidden_states,)
223
+
224
+ if self.gradient_checkpointing and self.training:
225
+ layer_outputs = self._gradient_checkpointing_func(
226
+ partial(decoder_layer.__call__, **flash_attn_kwargs),
227
+ hidden_states,
228
+ causal_mask,
229
+ position_ids,
230
+ past_key_values,
231
+ output_attentions,
232
+ output_docs_score,
233
+ use_cache,
234
+ cache_position,
235
+ position_embeddings,
236
+ doc_ids,
237
+ input_ids,
238
+ )
239
+ else:
240
+ layer_outputs = decoder_layer(
241
+ hidden_states,
242
+ attention_mask=causal_mask,
243
+ position_ids=position_ids,
244
+ past_key_value=past_key_values,
245
+ output_attentions=output_attentions,
246
+ output_docs_score=output_docs_score,
247
+ use_cache=use_cache,
248
+ cache_position=cache_position,
249
+ position_embeddings=position_embeddings,
250
+ doc_ids=doc_ids,
251
+ input_ids=input_ids,
252
+ **flash_attn_kwargs,
253
+ )
254
+
255
+ hidden_states = layer_outputs[0]
256
+
257
+ if output_attentions:
258
+ all_self_attns += (layer_outputs[1],)
259
+
260
+ if output_docs_score:
261
+ all_docs_scores += (layer_outputs[-1],)
262
+
263
+ hidden_states = self.norm(hidden_states)
264
+
265
+ # add hidden states from the last decoder layer
266
+ if output_hidden_states:
267
+ all_hidden_states += (hidden_states,)
268
+
269
+ return MSALayerModelOutputWithPast(
270
+ last_hidden_state=hidden_states,
271
+ past_key_values=past_key_values if use_cache else None,
272
+ hidden_states=all_hidden_states,
273
+ attentions=all_self_attns,
274
+ all_docs_scores=all_docs_scores
275
+ )
276
+
277
+ class MSAForCausalLM(Qwen3PreTrainedModel, MSAGenerationMixin):
278
+ config_class = MSAConfig
279
+ _tied_weights_keys = ["lm_head.weight"]
280
+ _tp_plan = {"lm_head": "colwise_rep"}
281
+ _pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
282
+
283
+ def __init__(self, config):
284
+ super().__init__(config)
285
+ self.num_layers = config.num_hidden_layers
286
+ self.router_layer_idx = config.msa_config.router_layer_idx
287
+
288
+ if self.router_layer_idx == "all":
289
+ self.router_layer_idx = list(range(config.num_hidden_layers))
290
+ else:
291
+ self.router_layer_idx = [int(i) for i in self.router_layer_idx.split(",")]
292
+
293
+ self.mid_layers = config.num_hidden_layers // 2
294
+ self.model = MSAModel(config)
295
+ self.vocab_size = config.vocab_size
296
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
297
+
298
+ self.aux_loss = config.msa_config.aux_loss
299
+ self.lmloss_weigth = config.msa_config.lmloss_weigth
300
+ self.auxloss_weight = config.msa_config.auxloss_weight
301
+ self.recloss_weight = config.msa_config.recloss_weight
302
+ self.ansloss_weight = config.msa_config.ansloss_weight
303
+ self.aux_loss_method = config.msa_config.aux_loss_method # INFONCE, BCE, INFONCE_DECOUPLE, INFONCE_DECOUPLE_FOCAL
304
+ self.decouple_router = config.msa_config.decouple_router
305
+
306
+ if "INFONCE" in self.aux_loss_method:
307
+ temperature = config.msa_config.infonce_loss_temp
308
+ self.temperature = nn.Parameter(torch.ones([]) * temperature, requires_grad=False)
309
+ elif self.aux_loss_method == "BCE":
310
+ self.b = nn.Parameter(-20 * torch.ones([]), requires_grad=True)
311
+ # Initialize weights and apply final processing
312
+ self.post_init()
313
+
314
+ def get_input_embeddings(self):
315
+ return self.model.embed_tokens
316
+
317
+ def set_input_embeddings(self, value):
318
+ self.model.embed_tokens = value
319
+
320
+ def get_output_embeddings(self):
321
+ return self.lm_head
322
+
323
+ def set_output_embeddings(self, new_embeddings):
324
+ self.lm_head = new_embeddings
325
+
326
+ def set_decoder(self, decoder):
327
+ self.model = decoder
328
+
329
+ def get_decoder(self):
330
+ return self.model
331
+
332
+ def forward(
333
+ self,
334
+ input_ids: Optional[torch.LongTensor] = None,
335
+ attention_mask: Optional[torch.Tensor] = None,
336
+ position_ids: Optional[torch.LongTensor] = None,
337
+ past_key_values: Optional[List[torch.FloatTensor]] = None,
338
+ inputs_embeds: Optional[torch.FloatTensor] = None,
339
+ labels: Optional[torch.LongTensor] = None,
340
+ use_cache: Optional[bool] = None,
341
+ output_attentions: Optional[bool] = None,
342
+ output_hidden_states: Optional[bool] = None,
343
+ output_docs_score: Optional[bool] = None,
344
+ cache_position: Optional[torch.LongTensor] = None,
345
+ logits_to_keep: Union[int, torch.Tensor] = 0,
346
+ # msa
347
+ doc_ids: Optional[torch.LongTensor] = None,
348
+ batch_aux_labels: List[List[int]] = None,
349
+ batch_reconstruction_labels: Optional[torch.LongTensor] = None,
350
+ batch_answer_labels: Optional[torch.LongTensor] = None,
351
+ train_qa_samples: Optional[torch.BoolTensor] = None,
352
+ **kwargs,
353
+ ) -> CausalLMOutputWithPast:
354
+ r"""
355
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
356
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
357
+ config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
358
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
359
+
360
+ logits_to_keep (`int` or `torch.Tensor`, *optional*):
361
+ If an `int`, compute logits for the last `logits_to_keep` tokens. If `0`, calculate logits for all
362
+ `input_ids` (special case). Only last token logits are needed for generation, and calculating them only for that
363
+ token can save memory, which becomes pretty significant for long sequences or large vocabulary size.
364
+ If a `torch.Tensor`, must be 1D corresponding to the indices to keep in the sequence length dimension.
365
+ This is useful when using packed tensor format (single dimension for batch and sequence length).
366
+
367
+ Returns:
368
+
369
+ Example:
370
+
371
+ ```python
372
+ >>> from transformers import AutoTokenizer, Qwen3ForCausalLM
373
+
374
+ >>> model = Qwen3ForCausalLM.from_pretrained("Qwen/Qwen3-8B")
375
+ >>> tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-8B")
376
+
377
+ >>> prompt = "Hey, are you conscious? Can you talk to me?"
378
+ >>> inputs = tokenizer(prompt, return_tensors="pt")
379
+
380
+ >>> # Generate
381
+ >>> generate_ids = model.generate(inputs.input_ids, max_length=30)
382
+ >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
383
+ "Hey, are you conscious? Can you talk to me?\nI'm not conscious, but I can talk to you."
384
+ ```"""
385
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
386
+ output_hidden_states = (
387
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
388
+ )
389
+
390
+ output_docs_score = self.aux_loss
391
+
392
+ # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn)
393
+ outputs = self.model(
394
+ input_ids=input_ids,
395
+ attention_mask=attention_mask,
396
+ position_ids=position_ids,
397
+ past_key_values=past_key_values,
398
+ inputs_embeds=inputs_embeds,
399
+ use_cache=use_cache,
400
+ output_attentions=output_attentions,
401
+ output_hidden_states=output_hidden_states,
402
+ output_docs_score=output_docs_score,
403
+ cache_position=cache_position,
404
+ doc_ids=doc_ids,
405
+ **kwargs,
406
+ )
407
+
408
+ hidden_states = outputs[0]
409
+
410
+ # Only compute necessary logits, and do not upcast them to float if we are not computing the loss
411
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
412
+ kept_hidden_states = hidden_states[:, slice_indices, :]
413
+
414
+ shift_labels = kwargs.pop("shift_labels", None)
415
+ logits = None
416
+ loss = None
417
+ reconstruction_loss = None
418
+ aux_loss = None
419
+ answer_loss = None
420
+ # if in training mode, don't materialize logits
421
+ if self.training and (labels is not None or shift_labels is not None):
422
+ loss = LigerForCausalLMLoss(
423
+ hidden_states=kept_hidden_states,
424
+ lm_head_weight=self.lm_head.weight,
425
+ labels=labels,
426
+ shift_labels=shift_labels,
427
+ hidden_size=self.config.hidden_size,
428
+ **kwargs,
429
+ )
430
+ if batch_reconstruction_labels is not None:
431
+ reconstruction_loss = LigerForCausalLMLoss(
432
+ hidden_states=kept_hidden_states,
433
+ lm_head_weight=self.lm_head.weight,
434
+ labels=batch_reconstruction_labels,
435
+ shift_labels=None,
436
+ hidden_size=self.config.hidden_size,
437
+ **kwargs,
438
+ )
439
+ else:
440
+ reconstruction_loss = torch.tensor(0.0).to(hidden_states.device)
441
+
442
+ if torch.sum(train_qa_samples) == 0:
443
+ batch_answer_labels = None
444
+ else:
445
+ train_qa_samples_mask = train_qa_samples == 1
446
+ temp_kept_hidden_states = kept_hidden_states[train_qa_samples_mask]
447
+ temp_batch_answer_labels = batch_answer_labels[train_qa_samples_mask]
448
+
449
+ if batch_answer_labels is not None:
450
+ answer_loss = LigerForCausalLMLoss(
451
+ hidden_states=temp_kept_hidden_states,
452
+ lm_head_weight=self.lm_head.weight,
453
+ labels=temp_batch_answer_labels,
454
+ shift_labels=None,
455
+ hidden_size=self.config.hidden_size,
456
+ **kwargs,
457
+ )
458
+ else:
459
+ answer_loss = torch.tensor(0.0).to(hidden_states.device)
460
+
461
+
462
+ else: # if in inference mode materialize logits
463
+ logits = self.lm_head(kept_hidden_states)
464
+ if labels is not None:
465
+ loss = self.loss_function(
466
+ logits=logits,
467
+ labels=labels,
468
+ vocab_size=self.config.vocab_size,
469
+ **kwargs,
470
+ )
471
+ if batch_reconstruction_labels is not None:
472
+ reconstruction_loss = self.loss_function(
473
+ logits=logits,
474
+ labels=batch_reconstruction_labels,
475
+ vocab_size=self.config.vocab_size,
476
+ **kwargs,
477
+ )
478
+ else:
479
+ reconstruction_loss = torch.tensor(0.0).to(hidden_states.device)
480
+
481
+ if batch_answer_labels is not None:
482
+ answer_loss = self.loss_function(
483
+ logits=logits,
484
+ labels=batch_answer_labels,
485
+ vocab_size=self.config.vocab_size,
486
+ **kwargs,
487
+ )
488
+ else:
489
+ answer_loss = torch.tensor(0.0).to(hidden_states.device)
490
+
491
+ lm_loss = torch.tensor(0.0).to(hidden_states.device)
492
+ if loss is not None:
493
+ lm_loss = loss.clone()
494
+ aux_loss = torch.tensor(0.0).to(hidden_states.device)
495
+ train_router_metrics = None
496
+ if batch_aux_labels is not None and self.aux_loss:
497
+ aux_loss, train_router_metrics = self.caculate_aux_loss(aux_loss, outputs, batch_aux_labels, hidden_states.device, hidden_states.dtype)
498
+
499
+ # Ensure all loss components have default values
500
+ reconstruction_loss = reconstruction_loss if reconstruction_loss is not None else torch.tensor(0.0).to(hidden_states.device)
501
+ answer_loss = answer_loss if answer_loss is not None else torch.tensor(0.0).to(hidden_states.device)
502
+ aux_loss = aux_loss if aux_loss is not None else torch.tensor(0.0).to(hidden_states.device)
503
+
504
+ if loss is not None:
505
+ loss = self.lmloss_weigth * loss + \
506
+ self.recloss_weight * reconstruction_loss + \
507
+ self.auxloss_weight * aux_loss + \
508
+ self.ansloss_weight * answer_loss
509
+
510
+ return MSACausalLMOutputWithPast(
511
+ loss=loss,
512
+ lm_loss=lm_loss,
513
+ aux_loss=aux_loss,
514
+ answer_loss=answer_loss,
515
+ reconstruction_loss=reconstruction_loss,
516
+ train_router_metrics=train_router_metrics,
517
+ logits=logits,
518
+ past_key_values=outputs.past_key_values,
519
+ hidden_states=outputs.hidden_states,
520
+ attentions=outputs.attentions,
521
+ temperature=self.temperature if "INFONCE" in self.aux_loss_method else torch.tensor(0.0).to(hidden_states.device),
522
+ )
523
+
524
+ def calculate_decoupled_infonce_loss(self, logits, label, num_pos):
525
+ """
526
+ 改进版:Decoupled InfoNCE
527
+ 解决了多正样本之间的互斥问题,检索任务推荐使用。
528
+ """
529
+ with torch.no_grad():
530
+ temperature = self.temperature.clamp(0.001, 0.5)
531
+
532
+ # 1. 缩放
533
+ scaled_logits = logits / temperature
534
+
535
+ # 2. 数值稳定:转为 exp 域
536
+ # 减去最大值防止溢出 (标准 Softmax trick)
537
+ max_logits = torch.max(scaled_logits, dim=0, keepdim=True)[0].detach()
538
+ exp_logits = torch.exp(scaled_logits - max_logits)
539
+
540
+ # 获取所有负样本的 exp 之和
541
+ neg_exp_sum = torch.sum(exp_logits * (1 - label), dim=0, keepdim=True)
542
+
543
+ # 4. 计算 Log Prob
544
+ # 对于每一个正样本 i:
545
+ # Prob_i = exp_i / (exp_i + neg_exp_sum)
546
+ # 这样分母里就没有“其他正样本”在竞争了
547
+ denominators = exp_logits + neg_exp_sum
548
+
549
+ log_probs = scaled_logits - max_logits - torch.log(denominators + 1e-10)
550
+
551
+ # 5. 计算 Loss
552
+ # 只取正样本位置的 log_prob
553
+ loss_map = - log_probs * (label / num_pos.clamp(min=1.0))
554
+
555
+ return loss_map.sum(dim=0)
556
+
557
+ def caculate_infonce_loss(self, logits, label, num_pos):
558
+ # 限制温度范围避免数值不稳定
559
+ with torch.no_grad():
560
+ temperature = self.temperature.clamp(0.001,0.5)
561
+ # 对logits进行温度缩放
562
+ scaled_logits = logits / temperature
563
+ # 确保正样本数至少为1,避免除零错误
564
+ safe_num_pos = num_pos.clamp(min=1.0)
565
+ # 将标签转换为与缩放后logits相同的浮点类型
566
+ aux_label_float = label.to(dtype=scaled_logits.dtype)
567
+ # 计算InfoNCE损失:负的对数softmax概率与标签的加权和
568
+ one_aux_loss = -torch.sum(F.log_softmax(scaled_logits, dim=0) * (aux_label_float / safe_num_pos), dim=0)
569
+ return one_aux_loss
570
+
571
+ def caculate_bce_loss(self, logits, label):
572
+ label[label==0] = -1 # 将 0 (负样本) 映射为 -1
573
+ one_aux_loss = -torch.mean(F.logsigmoid((logits + self.b) * label))
574
+ return one_aux_loss
575
+
576
+ def calculate_multi_pos_focal_infonce(self, logits, label, gamma=2.0):
577
+ """
578
+ Args:
579
+ logits: (Batch_Size, Num_Candidates) 或者是 (N, 1) 的形式
580
+ label: (Batch_Size, Num_Candidates) Multi-hot 标签,1为正,0为负
581
+ gamma: Focal 参数
582
+ """
583
+ with torch.no_grad():
584
+ temperature = self.temperature.clamp(0.001, 0.5)
585
+
586
+ # 1. 缩放 logits
587
+ scaled_logits = logits / temperature
588
+
589
+ # 2. 核心 Trick:数值稳定地将 logits 转为 exp 域
590
+ # 为了防止 exp 溢出,先减去最大值
591
+ max_logits = torch.max(scaled_logits, dim=0, keepdim=True)[0].detach()
592
+ exp_logits = torch.exp(scaled_logits - max_logits)
593
+
594
+ # 3. 构建分母 (Denominator)
595
+ # 传统的 InfoNCE 分母是 sum(exp_logits)
596
+ # 我们现在的分母应该是:当前正样本的 exp + 所有负样本的 exp
597
+ # 等价于:总和 - 其他正样本的 exp
598
+
599
+ sum_exp = torch.sum(exp_logits, dim=0, keepdim=True) # 所有样本的 exp 之和
600
+
601
+ # 利用 label (multi-hot) 找出所有正样本的 exp
602
+ # label 为 1 的位置是正样本
603
+ pos_exp = exp_logits * label
604
+ neg_exp_sum = torch.sum(exp_logits * (1 - label), dim=0, keepdim=True) # 所有负样本的 exp 之和
605
+
606
+ # 4. 计算每个正样本对应的 Softmax 概率
607
+ # 对于每一个位置 i (如果是正样本):
608
+ # Prob_i = exp_i / (exp_i + sum(exp_negatives))
609
+ # 注意:这里分母不包含 label 中其他的正样本!
610
+
611
+ # 这里利用广播机制:
612
+ # 分母 = 当前位置的 exp (如果是正样本) + 所有负样本的 sum_exp
613
+ denominators = exp_logits + neg_exp_sum
614
+
615
+ # 计算概率 P
616
+ probs = exp_logits / denominators
617
+
618
+ # 5. 计算 Log Softmax (数值更稳定)
619
+ # log(p) = logits - log(denominators)
620
+ # 同样使用数值稳定的 logaddexp 或者直接操作
621
+ # 这里为了代码清晰,直接对上面算出的 probs 取 log,实际工程中建议用 log_softmax 形式优化
622
+ log_probs = torch.log(probs + 1e-10)
623
+
624
+ # 6. Focal Weight
625
+ # w = (1 - p)^gamma
626
+ focal_weights = (1 - probs).pow(gamma)
627
+
628
+ # 7. 计算 Loss
629
+ # 只保留正样本位置的 Loss
630
+ loss_map = - focal_weights * log_probs * label
631
+
632
+ # 8. 归一化
633
+ # 除以正样本的总数
634
+ num_pos = label.sum()
635
+ loss = loss_map.sum() / (num_pos + 1e-6)
636
+ return loss
637
+
638
+ def calculate_focal_infonce_loss(self, logits, label, num_pos, gamma=2.0):
639
+ """
640
+ Args:
641
+ logits: 模型输出的 logits
642
+ label: 正样本的 mask (通常是 multi-hot)
643
+ num_pos: 正样本的数量
644
+ gamma: Focal Loss 的超参数,控制挖掘难样本的程度,通常设为 2.0
645
+ """
646
+ with torch.no_grad():
647
+ # 限制温度范围,防止数值不稳定
648
+ temperature = self.temperature.clamp(0.001, 0.5)
649
+
650
+ # 1. 缩放 Logits
651
+ scaled_logits = logits / temperature
652
+
653
+ # 2. 提前计算 Softmax 分数 (即公式中的 P = exp(s)/Z)
654
+ # dim=0 对应你的输入是单个向量的情况
655
+ probs = F.softmax(scaled_logits, dim=0)
656
+
657
+ # 3. 计算 Log Softmax (用于 Loss 计算,比 log(probs) 数值更稳定)
658
+ log_probs = F.log_softmax(scaled_logits, dim=0)
659
+
660
+ # 4. 计算 Focal Weight: w = (1 - p)^gamma
661
+ # 公式对应:w_i = (1 - p_i)^gamma
662
+ # 这里不对权重进行 detach,允许梯度回传以调整对难易样本的关注度 (参考原始 Focal Loss 论文)
663
+ focal_weights = (1 - probs).pow(gamma)
664
+
665
+ # 5. 准备 Label 和归一化项
666
+ safe_num_pos = num_pos.clamp(min=1.0)
667
+ aux_label_float = label.to(dtype=scaled_logits.dtype)
668
+
669
+ # 6. 计算加权的 InfoNCE Loss
670
+ # 公式对应:Sum [ -w_i * log(p_i) ]
671
+ # 这里利用 label (0/1) 来只保留正样本的 Loss,并除以 num_pos 做平均
672
+ weighted_loss = -torch.sum(
673
+ focal_weights * log_probs * (aux_label_float / safe_num_pos),
674
+ dim=0
675
+ )
676
+
677
+ return weighted_loss
678
+
679
+ def caculate_aux_loss(self, aux_loss, outputs, batch_aux_labels, device, dtype):
680
+ count = 0
681
+ all_layer_doc_score = outputs.all_docs_scores
682
+ train_router_metrics = {}
683
+ for layer_idx, layer_doc_score in enumerate(all_layer_doc_score):
684
+ # if self.mid_layers <= layer_idx < self.num_layers:
685
+ if self.mid_layers <= layer_idx and layer_idx in self.router_layer_idx:
686
+ for b in range(len(batch_aux_labels)):
687
+ aux_logits_full = layer_doc_score[b]
688
+ aux_label = torch.LongTensor(batch_aux_labels[b]).type(dtype).to(device)
689
+
690
+ # 1. 创建一个 mask 来找到所有有效的 (非 -inf) doc
691
+ valid_doc_mask = (aux_logits_full > -1e9) # 或 != -float('inf')
692
+
693
+ # 2. 使用 *同一个 mask* 来过滤 logits 和 labels
694
+ aux_logits = aux_logits_full[valid_doc_mask]
695
+
696
+ if aux_label.shape[0] == 0:
697
+ continue
698
+
699
+ num_pos = aux_label.sum()
700
+ if self.aux_loss_method == "BCE":
701
+ one_aux_loss = self.caculate_bce_loss(aux_logits, aux_label)
702
+ elif self.aux_loss_method == "INFONCE":
703
+ one_aux_loss = self.caculate_infonce_loss(aux_logits, aux_label, num_pos)
704
+ elif self.aux_loss_method == "INFONCE_FOCAL":
705
+ one_aux_loss = self.calculate_focal_infonce_loss(aux_logits, aux_label, num_pos)
706
+ elif self.aux_loss_method == "INFONCE_DECOUPLE":
707
+ one_aux_loss = self.calculate_decoupled_infonce_loss(aux_logits, aux_label, num_pos)
708
+ elif self.aux_loss_method == "INFONCE_DECOUPLE_FOCAL":
709
+ one_aux_loss = self.calculate_multi_pos_focal_infonce(aux_logits, aux_label, num_pos)
710
+
711
+ aux_loss += one_aux_loss
712
+ count += 1
713
+
714
+ # 计算recall
715
+ recall_at_n = [1, 5, 10]
716
+ for at_n in recall_at_n:
717
+ at_n = min(at_n, aux_logits.shape[0])
718
+ top_k_indices = torch.topk(aux_logits, at_n)[1].cpu().tolist()
719
+ hit_count = 0
720
+ for idx in top_k_indices:
721
+ if aux_label[idx] == 1:
722
+ hit_count += 1
723
+ recall_n = hit_count / min(num_pos.item(), 10)
724
+ if f'recall@{at_n}' not in train_router_metrics:
725
+ train_router_metrics[f'recall@{at_n}'] = [recall_n]
726
+ else:
727
+ train_router_metrics[f'recall@{at_n}'].append(recall_n)
728
+
729
+ if count != 0:
730
+ aux_loss = aux_loss / count
731
+ for k, v in train_router_metrics.items():
732
+ train_router_metrics[k] = sum(v) / len(v)
733
+ return aux_loss, train_router_metrics
src/msa_service.py ADDED
@@ -0,0 +1,1911 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import queue
2
+ import pickle
3
+ import json
4
+ from tqdm import tqdm
5
+ import torch
6
+ import torch.distributed as dist
7
+ import multiprocessing as mp
8
+ import threading
9
+ import os
10
+ from typing import Tuple, List, Optional, Dict, Union, Any, Callable
11
+ from dataclasses import dataclass
12
+ from transformers import AutoTokenizer, BitsAndBytesConfig, AutoConfig
13
+
14
+ from src.config.memory_config import GenerateConfig, MemoryConfig, ModelConfig
15
+ from src.prefill import PrefillStage1Worker
16
+ from src.types import ProtocolConstants
17
+ from src.utils.cache import create_cache, CustomDynamicCache
18
+ from src.utils.gpu_worker import GpuWorker
19
+ from src.utils.tools import RequestLimiter, format_bytes, cumulative_concat, compose_input
20
+ from src.msa.model import MSAForCausalLM, MSAConfig
21
+
22
+ # ==========================================
23
+ # 数据结构定义
24
+ # ==========================================
25
+
26
+ MEMORY_WORKER_READY = "MEMORY_WORKER_READY"
27
+ MEMORY_WORKER_CLOSE = "MEMORY_WORKER_CLOSE"
28
+ MEMORY_WORKER_BLOCKS = "MEMORY_WORKER_BLOCKS"
29
+ MEMORY_WORKER_IDX_TO_DOC = "MEMORY_WORKER_IDX_TO_DOC"
30
+ MEMORY_WORKER_REPORT_KV_STATES = "MEMORY_WORKER_REPORT_KV_STATES"
31
+
32
+ @dataclass
33
+ class CmdBase:
34
+ name: str = ""
35
+
36
+ @dataclass
37
+ class ResultBase:
38
+ name: str = ""
39
+ gpu_id: int = 0
40
+
41
+
42
+ @dataclass
43
+ class QueryTemplateKVPrefixCmd(CmdBase):
44
+ layer_idx: int = -1 # -1 indicate get all layers
45
+
46
+ def __post_init__(self):
47
+ self.name = "query_template_kv_prefix"
48
+
49
+ @dataclass
50
+ class QueryTemplateKVPrefixResult(ResultBase):
51
+ template_kvcache: Dict[int, Dict[str, torch.Tensor]] = None
52
+
53
+ def __post_init__(self):
54
+ self.name = "query_template_kv_prefix"
55
+
56
+ @dataclass
57
+ class PrefillStage2Cmd(CmdBase):
58
+ query_states: torch.Tensor = None
59
+ attention_mask: torch.Tensor = None
60
+ doc_ids: torch.Tensor = None
61
+ layer_index: int = 0
62
+
63
+ def __post_init__(self):
64
+ self.name = "prefill_stage2"
65
+
66
+ @dataclass
67
+ class PrefillStage2Result(ResultBase):
68
+ key: Union[torch.Tensor, List[torch.Tensor]] = None
69
+ value: Union[torch.Tensor, List[torch.Tensor]] = None
70
+ pooled_doc_ids: Union[torch.Tensor, List[torch.Tensor]] = None
71
+
72
+ def __post_init__(self):
73
+ self.name = "prefill_stage2"
74
+
75
+ # TODO: 这个可以取消,因为这是为了让prefill产生的 pooled doc id 全局唯一,但是这个完全可以
76
+ # 在归集stage2结果的时候才去做,只要用 block 或者 gpu id 做一个偏移就好
77
+ @dataclass
78
+ class SetDocIDCmd(CmdBase):
79
+ pooled_doc_id_bias: int = 0
80
+
81
+ def __post_init__(self):
82
+ self.name = "set_doc_id"
83
+
84
+ @dataclass
85
+ class ReportDocID(ResultBase):
86
+ pooled_doc_id_bias: int = 0
87
+
88
+ def __post_init__(self):
89
+ self.name = "report_doc_id"
90
+
91
+ class CustomDynamicCacheOnCPU(CustomDynamicCache):
92
+ def __init__(self, _distributed_cache_data=None):
93
+ super().__init__(_distributed_cache_data)
94
+
95
+ # def record_kwargs(self, layer_idx, kwargs):
96
+ # d = {}
97
+ # for k, v in kwargs.items():
98
+ # if v is not None and torch.is_tensor(v):
99
+ # d[k] = v.cpu() if v.is_cuda else v.clone()
100
+ # else:
101
+ # d[k] = v
102
+ # super().record_kwargs(layer_idx, kwargs)
103
+
104
+ def update(
105
+ self,
106
+ key_states: torch.Tensor,
107
+ value_states: torch.Tensor,
108
+ layer_idx: int,
109
+ cache_kwargs=None,
110
+ ) -> tuple[torch.Tensor, torch.Tensor]:
111
+ # if key_states is not None and torch.is_tensor(key_states) and key_states.is_cuda:
112
+ # key_states = key_states.cpu()
113
+ if value_states is not None and torch.is_tensor(value_states) and value_states.is_cuda:
114
+ value_states = value_states.cpu()
115
+ return super().update(key_states, value_states, layer_idx, cache_kwargs)
116
+
117
+ @dataclass
118
+ class GenerateRequest(CmdBase):
119
+ # 每次用户发送一个 generate 请求,我们都分配一个 msgid
120
+ # 但是我们会将其分成world个GenerateRequest,seq_id表示其自请求顺序
121
+ msg_id: int = 0
122
+ seq_id: int = 0
123
+ input_ids: List[int] = None
124
+ attention_mask: List[int] = None
125
+ doc_ids: List[int] = None
126
+ positions: List[int] = None
127
+ require_recall_topk: bool = False
128
+ prompts: List[str] = None
129
+
130
+ def __post_init__(self):
131
+ self.name = "generate_request"
132
+
133
+ @dataclass
134
+ class GenerateResponse():
135
+ msg_id: int = -1
136
+ seq_id: int = -1
137
+ gpu_id: int = 0
138
+ generated_texts: List[str] = None
139
+ recall_topk: Dict[int, List] = None
140
+
141
+
142
+ # response receiver: callback(generated_texts, required_recall_topk, userdata)
143
+ GenerateReceiver = Callable[[List[str], Any, Any], None]
144
+
145
+ @dataclass
146
+ class GenerateStub:
147
+ msg_id: int = 0
148
+ userdata: Any = None
149
+ responses: List[GenerateResponse] = None
150
+ nr_dummy: int = 0 # 标记这个 batch 尾部有几个query 是填充的空请求
151
+ callback: Optional[GenerateReceiver] = None
152
+
153
+ def respond(self):
154
+ if self.callback is None:
155
+ print("no callback")
156
+ return
157
+
158
+ self.responses.sort(key=lambda r: r.seq_id)
159
+ txts = []
160
+ recall_topk: Dict[int, List] = {}
161
+ for rsp in self.responses:
162
+ txts.extend(rsp.generated_texts)
163
+ if rsp.recall_topk:
164
+ for layer, topks in rsp.recall_topk.items():
165
+ if layer not in recall_topk:
166
+ recall_topk[layer] = []
167
+ recall_topk[layer].extend(topks)
168
+
169
+ self.callback(txts[:len(txts)-self.nr_dummy], recall_topk, self.userdata)
170
+
171
+
172
+
173
+
174
+
175
+ @dataclass
176
+ class Document:
177
+ doc: str = ""
178
+ doc_id: int = 0
179
+ num_chunks: int = 0
180
+
181
+ @dataclass
182
+ class BlockModelInput:
183
+ doc_input_ids: torch.Tensor
184
+ doc_attention_mask: torch.Tensor
185
+ doc_ids: torch.Tensor
186
+ position_ids: torch.Tensor
187
+ num_chunks: int
188
+ chunk_sizes: List[int]
189
+
190
+ @dataclass
191
+ class BlockDesc:
192
+ """一个 block 的描述数据"""
193
+ docs: List[Document] = None # 所有分配到此 GPU 上的文档
194
+ tmp_doc_ids: List[torch.Tensor] = None # 临时存储
195
+
196
+ # kvcache的每一个 chunk 分组编号,从 1 开始,形如[1,1,1,2,2,3,3,3,...]
197
+ # pool_ids[-1] 就是文档总数,相同的编号对应的 chunks 表示同属于同一个文档
198
+ pool_ids: torch.Tensor = None
199
+ # 用于通过 pool_id查找 global doc id,即 global_doc_ids = doc_ids[pool_ids]
200
+ doc_ids: torch.Tensor = None
201
+ doc_ids_cpu: torch.Tensor = None # doc_ids 的 CPU 版本,方便后续索引使用
202
+
203
+ # 用 local id,也就是pool_ids中的 id 来进行检索
204
+ doc_offsets_cpu: torch.Tensor = None # 每个文档 chunk 的起始索引 [nr_docs + 1]
205
+ doc_lens_cpu: torch.Tensor = None # 每个文档包含的 chunk 数量 [nr_docs + 1]
206
+
207
+ def init_docs(self, docs: List[Document], device):
208
+ self.docs = docs
209
+ self.doc_ids_cpu = torch.LongTensor([0] + [item.doc_id for item in self.docs])
210
+ self.doc_ids = self.doc_ids_cpu.to(device)
211
+ self.tmp_doc_ids = []
212
+
213
+ # 计算偏移量:假设 doc 0 长度为 0
214
+ lens = [0] + [doc.num_chunks for doc in docs]
215
+ self.doc_lens_cpu = torch.LongTensor(lens)
216
+ self.doc_offsets_cpu = torch.cumsum(torch.LongTensor([0] + lens[:-1]), dim=0)
217
+
218
+ def create_global_doc_ids(self, local_doc_ids):
219
+ return self.doc_ids[local_doc_ids]
220
+
221
+ def merge_poolig_doc_id(self, device):
222
+ self.pool_ids_cpu = cumulative_concat(self.tmp_doc_ids)
223
+ self.pool_ids = self.pool_ids_cpu.to(device)
224
+ self.tmp_doc_ids.clear()
225
+
226
+ def chunks(self):
227
+ return sum(doc.num_chunks for doc in self.docs)
228
+
229
+ @property
230
+ def nr_docs(self):
231
+ return len(self.docs)
232
+
233
+ @dataclass
234
+ class BlockData:
235
+ """本 GPU 上的文档 kvcache:[1, num_head, num_chunks, head_dim]"""
236
+ k: torch.Tensor = None # k 用于 attn 计算,如果 rk 不存在则保存于 GPU,同时用于检索,否则保存在 CPU
237
+ v: torch.Tensor = None # 存储于 CPU
238
+ rk: torch.Tensor = None # 用于检索,此时我们有独立的检索 k,需要保存于 GPU
239
+
240
+ def get_router_k(self):
241
+ """获取用于检索的k"""
242
+ return self.rk if self.rk is not None else self.k
243
+
244
+
245
+ @dataclass
246
+ class SliceDesc:
247
+ """block分片信息,每一个分片包含了一部分文档信息
248
+ """
249
+ nr_docs: int = 0 # 文档数量
250
+ nr_chunks: int = 0 # chunk数量
251
+ global_doc_ids: torch.Tensor = None # 文档 ID,shape 为[nr_chunks]
252
+
253
+ # 下面这几个 tensor 是我们预先计算好,用于 prefill stage2 的数据
254
+ local_doc_ids: torch.Tensor = None
255
+ local_doc_ids_0: torch.Tensor = None
256
+ original_doc_ids: torch.Tensor = None
257
+
258
+
259
+ class MemoryClientBase():
260
+ """被模型调用的接口"""
261
+ def get_template_prefix_kvcaches(self, layer_idx: int):
262
+ pass
263
+ def doc_query(self, query_states: torch.Tensor, attention_mask: torch.Tensor, layer_idx: int):
264
+ pass
265
+
266
+ # ==========================================
267
+ # Service 实现
268
+ # ==========================================
269
+ class Memory(GpuWorker):
270
+ """Memory工作进程"""
271
+
272
+ def __init__(self,
273
+ gpu_id: int,
274
+ generate_config: GenerateConfig,
275
+ model_config: ModelConfig,
276
+ memory_config: MemoryConfig):
277
+ """
278
+ 该 worker 被MemoryWorker创建并仅执行prefill stage 1获取block 的kv cache
279
+ """
280
+ super().__init__(gpu_id, model_config.get_model_envs())
281
+
282
+ self.model_config = model_config
283
+ self.generate_config = generate_config
284
+ self.memory_config = memory_config
285
+
286
+ self.msa_model_config = MSAConfig.from_pretrained(model_config.model_path)
287
+
288
+ self.router_layer_ids = list(range(self.num_model_layers()) if model_config.router_layer_idx == "all"
289
+ else map(int, model_config.router_layer_idx.split(",")))
290
+ self.router_layer_ids.sort()
291
+
292
+ self.num_key_value_groups = self.msa_model_config.num_attention_heads // self.msa_model_config.num_key_value_heads
293
+
294
+
295
+ self.vcache_in_cpu = True
296
+ self.v_device = 'cpu' if self.vcache_in_cpu else self.device
297
+
298
+ ######## 下面几个定义是可以动态加载进来的
299
+ # 每一个router层上的分片数据列表: layer_idx => [block 1, block 2, ...]
300
+ self.blocks: Dict[int, BlockData] = {layer: BlockData() for layer in self.router_layer_ids}
301
+ # 每一个 block 的描述数据, [block 1 desc, block 2 desc, ...]
302
+ self.block_desc: BlockDesc = BlockDesc()
303
+ # 每一层上的template prefix kv cache: layer idx -> (k, v)
304
+ self.template_prefix_kvcache: Dict[int, Tuple[torch.Tensor, torch.Tensor]] = {}
305
+
306
+ # 对 block 进行chunk 分割时,每个 block 大小
307
+ self.slice_chunk_size = memory_config.slice_chunk_size
308
+ # 为了后续计算方便,我们预先处理好了 k slice,shape 为[1, num_kv_heads, 1, hdim, chunk]
309
+ self.k_slices: Dict[int, List[torch.Tensor]] = []
310
+ self.slice_desc: List[SliceDesc] = []
311
+
312
+ self.head_reduce_method = self.msa_model_config.msa_config.head_reduce_method
313
+ self.query_reduce_method = self.msa_model_config.msa_config.query_reduce_method
314
+ self.chunk_reduce_method = self.msa_model_config.msa_config.chunk_reduce_method
315
+ self.aux_loss_method = self.msa_model_config.msa_config.aux_loss_method
316
+ self.decouple_router = self.msa_model_config.msa_config.decouple_router
317
+
318
+ if self.aux_loss_method == "INFONCE" and self.decouple_router:
319
+ self.scaling = -1.0
320
+ else:
321
+ self.scaling = self.msa_model_config.head_dim**-0.5
322
+
323
+ def _start_worker(self):
324
+ self._worker_req_q, self._worker_rsp_q = mp.Queue(), mp.Queue()
325
+
326
+ self._worker_process = mp.Process(
327
+ target=PrefillStage1Worker.prefill_worker_main,
328
+ args=(self.gpu_id, self._worker_req_q, self._worker_rsp_q,
329
+ self.model_config.model_path, self.generate_config.template,
330
+ self.memory_config.pooling_kernel_size, self.memory_config.block_size,
331
+ self.model_config.get_model_envs()),
332
+ name=f"PrefillStage1Worker-{self.gpu_id}"
333
+ )
334
+ self._worker_process.start()
335
+
336
+ PrefillStage1Worker.wait_for_ready(self._worker_rsp_q)
337
+ print(f"Prefill worker {self.gpu_id} is ready")
338
+
339
+ def _stop_worker(self):
340
+ print(f"Stop prefill worker on GPU {self.gpu_id}")
341
+ PrefillStage1Worker.close_worker(self._worker_req_q)
342
+ self._worker_process.join()
343
+ self._worker_process = None
344
+ self._worker_req_q, self._worker_rsp_q = None, None
345
+
346
+
347
+ def num_model_layers(self):
348
+ return self.msa_model_config.num_hidden_layers
349
+
350
+
351
+ def serialize(self, path: str):
352
+ """
353
+ 安全分片序列化:
354
+ 1. 创建目录
355
+ 2. 保存元数据 (meta.pt)
356
+ 3. 逐层保存 Tensor 到独立文件,避免内存峰值
357
+ """
358
+ # 1. 准备目录 (如果存在则警告或清空,这里选择清空以确保一致性)
359
+ if os.path.exists(path):
360
+ print(f"Warning: Directory {path} exists. Overwriting data inside.")
361
+ else:
362
+ os.makedirs(path, exist_ok=True)
363
+
364
+ stub = {
365
+ "memory_file_path": self.memory_config.memory_file_path,
366
+ "world": self.generate_config.world,
367
+ }
368
+ with open(os.path.join(path, "stub.json"), "w") as fp:
369
+ json.dump(stub, fp)
370
+
371
+ # 2. 保存元数据 (BlockDesc + 结构信息)
372
+ # 注意:BlockData 里的 heavy tensor 不在这里存,只存结构
373
+ meta_dict = {
374
+ "router_layer_ids": self.router_layer_ids,
375
+ "block_desc": {
376
+ "docs": self.block_desc.docs,
377
+ "doc_ids": self.block_desc.doc_ids.cpu(),
378
+ # pool_ids 转 CPU 保存
379
+ "pool_ids": self.block_desc.pool_ids.cpu() if self.block_desc.pool_ids is not None else None
380
+ },
381
+ }
382
+ torch.save(meta_dict, os.path.join(path, "meta.pt"))
383
+
384
+ # 3. 逐层保存 Blocks (重头戏)
385
+ print("Serializing Blocks (Sharded)...")
386
+ for layer_id, block in tqdm(self.blocks.items(), desc="Saving Layers"):
387
+ # 分离存储 K 和 V
388
+ if block.k is not None:
389
+ # K 是 GPU -> CPU
390
+ torch.save(block.k.cpu(), os.path.join(path, f"layer_{layer_id}_k.pt"))
391
+
392
+ if block.v is not None:
393
+ # V 已经在 CPU,直接存
394
+ # 提示:对于 CPU 上的极大 Tensor,torch.save 效率很高
395
+ torch.save(block.v.cpu(), os.path.join(path, f"layer_{layer_id}_v.pt"))
396
+
397
+ if block.rk is not None:
398
+ torch.save(block.rk.cpu(), os.path.join(path, f"layer_{layer_id}_rk.pt"))
399
+
400
+ # 4. 保存 Prefix Cache
401
+ if self.template_prefix_kvcache:
402
+ print("Serializing Prefix Cache...")
403
+ prefix_data = {}
404
+ for layer_id, (k, v) in self.template_prefix_kvcache.items():
405
+ prefix_data[layer_id] = (k.cpu(), v.cpu())
406
+ torch.save(prefix_data, os.path.join(path, "prefix_cache.pt"))
407
+
408
+ print(f"Serialization complete. Data saved to {path}")
409
+
410
+ def deserialize(self, path: str):
411
+ """
412
+ 流式反序列化:
413
+ 每次只加载一个文件进内存,处理完后立即释放或转移到 GPU,
414
+ 将系统内存(RAM)占用控制在最低。
415
+ """
416
+ if not os.path.exists(path):
417
+ raise FileNotFoundError(f"Path {path} does not exist.")
418
+
419
+ print("Deserialize from ", path)
420
+
421
+ # 1. 加载元数据
422
+ meta_path = os.path.join(path, "meta.pt")
423
+ meta = torch.load(meta_path, map_location="cpu", weights_only=False)
424
+
425
+ # 恢复 BlockDesc
426
+ bd_info = meta["block_desc"]
427
+ self.block_desc = BlockDesc(
428
+ docs=bd_info["docs"],
429
+ doc_ids=bd_info["doc_ids"].to(self.device),
430
+ pool_ids=bd_info["pool_ids"].to(self.device) if bd_info["pool_ids"] is not None else None
431
+ )
432
+
433
+ # 2. 逐层恢复 Blocks
434
+ # 关键:我们不一次性加载所有文件,而是循环加载
435
+ self.blocks = {}
436
+ print(f"Deserializing Blocks to GPU {self.gpu_id}...")
437
+
438
+ for layer_id in tqdm(self.router_layer_ids, desc=f"GPU-{self.gpu_id} Loading Layers", position=self.gpu_id):
439
+ k_path = os.path.join(path, f"layer_{layer_id}_k.pt")
440
+ v_path = os.path.join(path, f"layer_{layer_id}_v.pt")
441
+ rk_path = os.path.join(path, f"layer_{layer_id}_rk.pt")
442
+
443
+ current_k = None
444
+ current_v = None
445
+ current_rk = None
446
+
447
+ # 加载 K -> 立即转 GPU -> 删除 CPU 副本
448
+ if os.path.exists(k_path):
449
+ temp_k = torch.load(k_path, map_location="cpu", weights_only=False)
450
+ current_k = temp_k.to(self.device) # 移入显存
451
+ del temp_k # 立即释放内存
452
+
453
+ # 加载 V -> 驻留 CPU
454
+ if os.path.exists(v_path):
455
+ # 既然 V 是 CPU 存储且极大,直接 load 即可
456
+ # 如果 V 极其巨大(比如超过 RAM),可以考虑 mmap=True,
457
+ # 但这要求保存时格式兼容,且 V 通常作为 cache 需要频繁读取,RAM 还是最快的
458
+ current_v = torch.load(v_path, map_location="cpu", weights_only=False)
459
+
460
+ if os.path.exists(rk_path):
461
+ current_rk = torch.load(rk_path, map_location="cpu", weights_only=False)
462
+
463
+ self.blocks[layer_id] = BlockData(k=current_k, v=current_v, rk=current_rk)
464
+
465
+ # 显式触发垃圾回收(可选,但在内存吃紧时有用)
466
+ # import gc; gc.collect()
467
+
468
+ # 3. 恢复 Prefix Cache
469
+ prefix_path = os.path.join(path, "prefix_cache.pt")
470
+ if os.path.exists(prefix_path):
471
+ prefix_raw = torch.load(prefix_path, map_location="cpu", weights_only=False)
472
+ self.template_prefix_kvcache = {}
473
+ for layer_id, (k_cpu, v_cpu) in prefix_raw.items():
474
+ self.template_prefix_kvcache[layer_id] = (
475
+ k_cpu.to(self.device),
476
+ v_cpu.to(self.device)
477
+ )
478
+
479
+ print("Deserialization complete.")
480
+
481
+ def _init_block_data(self, shape: torch.Size, dtype: torch.dtype, has_rk=False):
482
+ total_chunks = self.block_desc.chunks()
483
+ for layer_idx in self.router_layer_ids:
484
+ block_data = self.blocks[layer_idx]
485
+
486
+ if block_data.k is None:
487
+ kv_shape = list(shape)
488
+ kv_shape[2] = total_chunks
489
+ # TODO: deserialize 时需要考虑 pin_memory
490
+ block_data.k = torch.empty(kv_shape, device=self.v_device if has_rk else self.device, dtype=dtype, pin_memory=has_rk)
491
+ block_data.v = torch.empty(kv_shape, device=self.v_device, dtype=dtype, pin_memory=True)
492
+ block_data.rk = torch.empty(kv_shape, device=self.device, dtype=dtype) if has_rk else None
493
+
494
+ def save_idx_to_doc(self, idx_to_doc: Dict[int, str]):
495
+ self.idx_to_doc = idx_to_doc
496
+
497
+ def generate_blocks(self, docs: List[Document]):
498
+ memory_data_path = os.environ.get("MEMORY_DATA_PATH", "")
499
+ if memory_data_path:
500
+ memory_data_path = os.path.join(memory_data_path, f"gpu{self.generate_config.world}", str(self.gpu_id))
501
+ if os.path.exists(memory_data_path):
502
+ self.deserialize(memory_data_path)
503
+ assert len(docs) == self.block_desc.nr_docs
504
+ self._post_process()
505
+ return
506
+
507
+ self._start_worker()
508
+
509
+ self.block_desc.init_docs(docs, self.device)
510
+
511
+ PrefillStage1Worker.send_documents(self._worker_req_q, docs)
512
+
513
+ kv_offset = 0
514
+ recv = 0
515
+
516
+ total_docs = len(docs)
517
+ total_chunks = self.block_desc.chunks()
518
+ pbar = tqdm(total=total_docs, desc=f"Worker-{self.gpu_id} memory inference", position=self.gpu_id)
519
+ while recv < total_docs:
520
+ meta = PrefillStage1Worker.recv_meta(self._worker_rsp_q)
521
+
522
+ recv += meta['nr_docs'] # 此 meta 包含了几个文档
523
+ pbar.update(meta['nr_docs'])
524
+
525
+ past_key_values: CustomDynamicCacheOnCPU = meta["past_key_values"]
526
+
527
+ # 仅保存一次
528
+ if not self.template_prefix_kvcache:
529
+ kwargs = past_key_values.cache_kwargs
530
+ self.template_prefix_kvcache = {
531
+ layer_idx: (
532
+ kwargs[layer_idx]['template_prefix_kcache'].to(self.device),
533
+ kwargs[layer_idx]['template_prefix_vcache'].to(self.device)
534
+ )
535
+ for layer_idx in range(self.num_model_layers())
536
+ }
537
+
538
+
539
+ meta_chunks = 0 # 这次的 meta 有多少 chunks
540
+
541
+ for layer_idx in self.router_layer_ids:
542
+ block_data = self.blocks[layer_idx]
543
+
544
+ k, v = past_key_values.get_kvcache(layer_idx) # for attn calc
545
+ rk = past_key_values.get_router_kcache(layer_idx) # for router, if any
546
+
547
+ desc: BlockDesc = self.block_desc
548
+ if block_data.k is None:
549
+ self._init_block_data(k.shape, k.dtype, has_rk=rk is not None)
550
+
551
+ if layer_idx == self.router_layer_ids[0]:
552
+ # 聚合 pooling doc id,因为每一层上的 pooling doc id 都是一样的,所以只聚合第一层
553
+ desc.tmp_doc_ids.append(past_key_values.cache_kwargs[layer_idx]['pooled_doc_ids'])
554
+
555
+ k_cache, v_cache, rk_cache = block_data.k, block_data.v, block_data.rk
556
+ if meta_chunks == 0:
557
+ meta_chunks = k.shape[2]
558
+ else:
559
+ assert meta_chunks == k.shape[2], f"expect meta_chunks {meta_chunks} but got {k.shape[2]}"
560
+ assert kv_offset + meta_chunks <= total_chunks, f"overflow: offset {kv_offset}, meta_chunks {meta_chunks}, total_chunks {total_chunks}"
561
+
562
+ # print(f"shape {k.shape} offset {kv_offset}")
563
+ k_cache[:, :, kv_offset:kv_offset+meta_chunks] = k.to(k_cache.device)
564
+ v_cache[:, :, kv_offset:kv_offset+meta_chunks] = v.to(v_cache.device)
565
+ if rk is not None:
566
+ rk_cache[:, :, kv_offset:kv_offset+meta_chunks] = rk.to(rk_cache.device)
567
+
568
+ # 每次一个 meta 处理完才需要进偏移
569
+ kv_offset += meta_chunks
570
+
571
+ past_key_values.clear_kvcache()
572
+
573
+ pbar.close()
574
+ self._stop_worker()
575
+
576
+ # 合并临时pooled doc id
577
+ self.block_desc.merge_poolig_doc_id(self.device)
578
+
579
+ if memory_data_path:
580
+ self.serialize(memory_data_path)
581
+
582
+ self._post_process()
583
+
584
+ def _post_process(self):
585
+ # 产生切片
586
+ self._generate_slice(self.slice_chunk_size)
587
+
588
+ kv_bytes = self.blocks[self.router_layer_ids[0]].k.nbytes * len(self.router_layer_ids)
589
+ if not self.vcache_in_cpu:
590
+ kv_bytes *= 2
591
+
592
+ kv_shape = self.blocks[self.router_layer_ids[0]].k.shape
593
+
594
+ print(f"GPU {self.gpu_id} memory usage: {format_bytes(kv_bytes)} kv shape {kv_shape}")
595
+ return kv_bytes, kv_shape
596
+
597
+ @staticmethod
598
+ def map_tensor_to_group_ids(a: torch.Tensor) -> torch.Tensor:
599
+ """
600
+ 根据相邻元素是否相同,将输入 Tensor a 的值映射到递增的组 ID Tensor b。
601
+
602
+ a[i] == a[i-1] => b[i] = b[i-1]
603
+ a[i] != a[i-1] => b[i] = b[i-1] + 1
604
+ b[0] 始终为 1。
605
+
606
+ Args:
607
+ a (torch.Tensor): 待处理的一维 int Tensor,必须在 GPU 上。
608
+
609
+ Returns:
610
+ torch.Tensor: 生成的组 ID Tensor b,在 GPU 上。
611
+ """
612
+ if a.ndim != 1:
613
+ raise ValueError("输入 Tensor a 必须是一维的。")
614
+
615
+ # 1. 检查相邻元素是否不同 (a[i] != a[i-1])
616
+ # torch.diff(a) 计算相邻元素之间的差值 a[i] - a[i-1]
617
+ # 差值不为 0 (!= 0) 即表示 a[i] != a[i-1]
618
+ # 结果是一个布尔 Tensor,长度比 a 短 1,代表从 a[1] 开始的相邻比较
619
+ diff_mask = torch.diff(a) != 0 # [L-1]
620
+
621
+ # 2. 将布尔值转换为整数 (0 或 1)
622
+ # True -> 1 (需要增加 ID),False -> 0 (保持 ID)
623
+ # torch.diff 的结果长度是 L-1,我们只关心从 a[1] 开始的递增
624
+ id_increments = diff_mask.int() # [L-1]
625
+
626
+ # 3. 累计增量
627
+ # torch.cumsum 对增量进行累加:
628
+ # 结果: [0, 0+1, 0+1+0, ...]
629
+ # 长度仍然是 L-1
630
+ group_indices_offset = torch.cumsum(id_increments, dim=0) # [L-1]
631
+
632
+ # 4. 构造最终的 Tensor b
633
+ # b[0] 始终是 1
634
+ # 对于 i > 0,b[i] = group_indices_offset[i-1] + 1
635
+
636
+ # 在 group_indices_offset 前面添加一个 0 作为 a[0] 的基准偏移
637
+ # [0, group_indices_offset[0], group_indices_offset[1], ...]
638
+ # 结果长度为 L
639
+ b = torch.cat((
640
+ torch.tensor([0], device=a.device, dtype=a.dtype),
641
+ group_indices_offset
642
+ )) + 1
643
+ return b
644
+ def _generate_slice(self, chunks):
645
+ nr_doc_slices = [] # 每个 block slice 所含的文档数量
646
+ nr_chunk_slices = [] # 每个 block slice 所含的 chunk 数量
647
+ chunk_slices_offset = [] # 每个 block slice 所含的 chunk 位置
648
+ chunk_slice_position: Tuple[int, int] = [] # 每个 chunk slice 的起始和结束位置
649
+
650
+ current_chunks = 0
651
+ current_docs = 0
652
+ chunk_offset = 0
653
+ for doc in self.block_desc.docs:
654
+ current_chunks += doc.num_chunks
655
+ current_docs += 1
656
+ chunk_offset += doc.num_chunks
657
+
658
+ if current_chunks >= chunks:
659
+ nr_doc_slices.append(current_docs)
660
+ nr_chunk_slices.append(current_chunks)
661
+ chunk_slices_offset.append(chunk_offset)
662
+ current_chunks = 0
663
+ current_docs = 0
664
+
665
+ if current_chunks > 0:
666
+ if current_chunks < 1024 and len(nr_doc_slices) > 0:
667
+ # too small, append to last
668
+ nr_doc_slices[-1] += current_docs
669
+ nr_chunk_slices[-1] += current_chunks
670
+ chunk_slices_offset[-1] = chunk_offset
671
+ else:
672
+ nr_doc_slices.append(current_docs)
673
+ nr_chunk_slices.append(current_chunks)
674
+ chunk_slices_offset.append(chunk_offset)
675
+
676
+ starts = [0] + chunk_slices_offset[:-1]
677
+ chunk_slice_position = [(start, end) for start, end in zip(starts, chunk_slices_offset)]
678
+
679
+ self.k_slices = {}
680
+ self.slice_desc = []
681
+ for slice_idx, (start, end) in enumerate(chunk_slice_position):
682
+ desc = SliceDesc(nr_docs=nr_doc_slices[slice_idx],
683
+ nr_chunks=nr_chunk_slices[slice_idx],
684
+ global_doc_ids=self.block_desc.pool_ids[start:end],
685
+ local_doc_ids=Memory.map_tensor_to_group_ids(self.block_desc.pool_ids[start:end]))
686
+ desc.local_doc_ids_0 = (desc.local_doc_ids - 1).view(1, -1)
687
+ unique_indices = torch.cat([torch.tensor([0], device=self.device), torch.where(desc.local_doc_ids[1:] != desc.local_doc_ids[:-1])[0] + 1])
688
+ desc.original_doc_ids = desc.global_doc_ids[unique_indices].view(1, -1)
689
+
690
+ self.slice_desc.append(desc)
691
+
692
+ for layer_idx in self.router_layer_ids:
693
+ rk = self.blocks[layer_idx].get_router_k()
694
+ slice = [rk[:,:,start:end].transpose(-1, -2).unsqueeze(2) for start, end in chunk_slice_position]
695
+ self.k_slices[layer_idx] = slice
696
+
697
+ def get_max_local_pool_doc_id(self) -> List[int]:
698
+ return self.block_desc.pool_ids[-1].item()
699
+
700
+ def get_template_prefix_kvcaches(self, layer_idx: int):
701
+ return self.template_prefix_kvcache[layer_idx]
702
+
703
+ def prefill_stage2(self, layer_idx: int, query_states: torch.Tensor, query_mask: torch.Tensor):
704
+ # query_mask: [bsz, 1, 1, seqlen, 1]
705
+ # query_states: [bsz, n_kv_head, n_kv_groups, seqlen, hdim]
706
+
707
+ bsz, num_kv_heads, num_kv_groups, seq_len, head_dim = query_states.shape
708
+ min_val = torch.finfo(query_states.dtype).min
709
+ routing_mask = ~query_mask
710
+ query_mask = query_mask.squeeze(2) # [bsz, 1, seqlen, 1]
711
+ q_lens = None
712
+
713
+ # 初始化分数表
714
+ # 严重注意:请确认 nr_docs 是否真的等于 max_doc_id + 1。
715
+ # 如果 pool_ids 是全局唯一的(例如加了 bias 100000),这里的 total_docs 必须能容纳最大的 ID。
716
+ total_docs = self.block_desc.nr_docs + 1
717
+ global_doc_scores = torch.full((bsz, total_docs), min_val, dtype=query_states.dtype, device=self.device)
718
+
719
+ # --- Phase 1: 打分 (移除 Query Batch Loop) ---
720
+
721
+ # 针对 A100,如果 bsz < 256,直接全量计算收益最高
722
+ # 我们直接遍历 slice,不再对 query 进行切片
723
+
724
+ for slice_desc, k_slice_t in zip(self.slice_desc, self.k_slices[layer_idx]):
725
+ # k_slice_t: [1, num_kv_heads, 1, hdim, chunk] (GPU)
726
+
727
+ # 1. Matmul (Compute Bound)
728
+ # [bsz, num_kv_heads, groups, seqlen, hdim] @ [1, num_kv_heads, 1, hdim, chunk]
729
+ # -> [bsz, num_kv_heads, groups, seqlen, chunk]
730
+ attn_scores = torch.matmul(query_states, k_slice_t)
731
+
732
+ # 2. Mask & Reduce (Memory Bound)
733
+ attn_scores.masked_fill_(routing_mask, min_val)
734
+ # Flatten & Reduce heads/seqlen
735
+ # flatten(1,2): [bsz, num_kv_heads*groups, seqlen, chunk]
736
+ # max(dim=2)[0]: [bsz, num_kv_heads*groups, chunk]
737
+ # max(dim=1)[0]: [bsz, chunk]
738
+ # max_scores_per_chunk = attn_scores.flatten(1, 2).max(dim=2)[0].max(dim=1)[0] # [bsz, chunk]
739
+ max_scores_per_chunk = attn_scores.flatten(1, 2)
740
+
741
+ # [bsz, num_kv_heads*groups, chunk]
742
+ if self.head_reduce_method == "max":
743
+ max_scores_per_chunk = max_scores_per_chunk.max(dim=1).values
744
+ elif self.head_reduce_method == "mean":
745
+ max_scores_per_chunk = max_scores_per_chunk.mean(dim=1)
746
+ else:
747
+ raise NotImplementedError(f"Unsupported head reduce method: {self.head_reduce_method}")
748
+
749
+ if self.query_reduce_method == "max":
750
+ max_scores_per_chunk = max_scores_per_chunk.max(dim=1).values
751
+ elif self.query_reduce_method == "mean":
752
+ # 1. 调整 mask 形状以匹配 max_scores_per_chunk
753
+ # [bsz, 1, seqlen, 1] -> [bsz, seqlen, 1]
754
+ mask = query_mask.squeeze(1)
755
+
756
+ # 2. 在求和前,将无效位置(min_val)归零
757
+ # 使用 torch.where 比乘法更安全,且能有效处理极小的 min_val
758
+ masked_sum = torch.where(mask, max_scores_per_chunk, torch.zeros_like(max_scores_per_chunk)).sum(dim=1)
759
+
760
+ # 3. 计算每个 sequence 中的有效 token 数量
761
+ # 防止除以 0,使用 clamp 或加一个极小值 eps
762
+ valid_counts = mask.sum(dim=1).clamp(min=1.0)
763
+
764
+ # 4. 得到最终均值 [bsz, chunk]
765
+ max_scores_per_chunk = masked_sum / valid_counts
766
+ elif self.query_reduce_method == "last":
767
+ if q_lens is None:
768
+ q_lens = query_mask.squeeze(3).squeeze(1).sum(dim=1).long()
769
+
770
+ # 边界保护: 如果 length 为 0 (异常情况), clamp 到 0
771
+ last_indices = (q_lens - 1).clamp(min=0)
772
+
773
+ # 3. 调整形状用于 gather
774
+ # [B, 1, C]
775
+ gather_idx = last_indices.view(bsz, 1, 1).expand(-1, 1, slice_desc.nr_chunks)
776
+
777
+ # 4. 提取
778
+ max_scores_per_chunk = max_scores_per_chunk.gather(1, gather_idx).squeeze(1)
779
+ else:
780
+ raise NotImplementedError(f"Unsupported query reduce method: {self.query_reduce_method}")
781
+
782
+ # 3. Local Scatter (Chunk -> Doc)
783
+ # slice_desc.local_doc_ids_0 已经在 GPU 上预计算好了
784
+ # [1, chunk] -> [bsz, chunk]
785
+ scatter_indices = slice_desc.local_doc_ids_0.expand(bsz, -1)
786
+
787
+ local_doc_scores = torch.full((bsz, slice_desc.nr_docs), -float('inf'), device=self.device, dtype=query_states.dtype)
788
+ if self.chunk_reduce_method == "max":
789
+ local_doc_scores.scatter_reduce_(
790
+ dim=1, index=scatter_indices, src=max_scores_per_chunk,
791
+ reduce="amax", include_self=True
792
+ )
793
+
794
+ elif self.chunk_reduce_method == "mean":
795
+ doc_sums = torch.zeros_like(local_doc_scores)
796
+ doc_sums = doc_sums.scatter_reduce(
797
+ dim=1, index=scatter_indices, src=max_scores_per_chunk,
798
+ reduce="sum", include_self=False
799
+ )
800
+ doc_counts = torch.zeros_like(local_doc_scores)
801
+ ones = torch.ones_like(max_scores_per_chunk)
802
+ doc_counts = doc_counts.scatter_reduce(
803
+ dim=1, index=scatter_indices, src=ones,
804
+ reduce="sum", include_self=False
805
+ )
806
+ doc_counts = doc_counts.clamp(min=1.0)
807
+ mean_scores = doc_sums / doc_counts
808
+ # TODO: 能不能不要重新产生一个local_doc_scores?
809
+ local_doc_scores = torch.where(doc_counts > 0, mean_scores, local_doc_scores)
810
+
811
+ del doc_sums, doc_counts, mean_scores
812
+ else:
813
+ raise ValueError(f"Invalid chunk reduction method: {self.chunk_reduce_method}")
814
+
815
+ # 4. Global Scatter (Local Doc -> Global Doc)
816
+ global_indices = slice_desc.original_doc_ids.expand(bsz, -1)
817
+ global_doc_scores.scatter_reduce_(
818
+ dim=1, index=global_indices, src=local_doc_scores,
819
+ reduce="amax", include_self=True
820
+ )
821
+ del attn_scores, max_scores_per_chunk, local_doc_scores
822
+
823
+ # --- Phase 2: Top-K 选择 (使用 topk 替代 sort) ---
824
+
825
+ # 优化点:topk 比 sort 快得多
826
+ final_scores, batch_selected_doc_ids = torch.topk(global_doc_scores, k=min(self.model_config.doc_top_k, global_doc_scores.shape[1]), dim=1) # [bsz, k]
827
+ batch_selected_global_doc_ids = self.block_desc.create_global_doc_ids(batch_selected_doc_ids)
828
+ return final_scores, batch_selected_doc_ids, batch_selected_global_doc_ids
829
+
830
+ def prefill_stage2_1(self, layer_idx: int, query_states: torch.Tensor, query_mask: torch.Tensor):
831
+ # ... [Timer Start] ...
832
+
833
+ # 优化点:Mask可以稍后处���,先让 Query 进计算
834
+
835
+ bsz, num_heads, seq_len, head_dim = query_states.shape
836
+ num_kv_groups = self.num_key_value_groups
837
+ num_kv_heads = num_heads // num_kv_groups
838
+ min_val = torch.finfo(query_states.dtype).min
839
+ routing_mask = ~query_mask
840
+ q_lens = None
841
+
842
+ # 1. View 重塑 (Zero-copy)
843
+ # TODO: scaling 计算
844
+ scaling = self.scaling
845
+ query_states = query_states.view(bsz, num_kv_heads, num_kv_groups, seq_len, head_dim) * scaling
846
+
847
+ # 初始化分数表
848
+ # 严重注意:请确认 nr_docs 是否真的等于 max_doc_id + 1。
849
+ # 如果 pool_ids 是全局唯一的(例如加了 bias 100000),这里的 total_docs 必须能容纳最大的 ID。
850
+ total_docs = self.block_desc.nr_docs + 1
851
+ global_doc_scores = torch.full((bsz, total_docs), min_val, dtype=query_states.dtype, device=self.device)
852
+
853
+ # --- Phase 1: 打分 (移除 Query Batch Loop) ---
854
+
855
+ # 针对 A100,如果 bsz < 256,直接全量计算收益最高
856
+ # 我们直接遍历 slice,不再对 query 进行切片
857
+
858
+ for slice_desc, k_slice_t in zip(self.slice_desc, self.k_slices[layer_idx]):
859
+ # k_slice_t: [1, nhead, 1, hdim, chunk] (GPU)
860
+
861
+ # 1. Matmul (Compute Bound)
862
+ # [bsz, nhead, groups, seqlen, hdim] @ [1, nhead, 1, hdim, chunk]
863
+ # -> [bsz, nhead, groups, seqlen, chunk]
864
+ attn_scores = torch.matmul(query_states, k_slice_t)
865
+
866
+ # 2. Mask & Reduce (Memory Bound)
867
+ attn_scores.masked_fill_(routing_mask, min_val)
868
+ # Flatten & Reduce heads/seqlen
869
+ # flatten(1,2): [bsz, nhead*groups, seqlen, chunk]
870
+ # max(dim=2)[0]: [bsz, nhead*groups, chunk]
871
+ # max(dim=1)[0]: [bsz, chunk]
872
+ # max_scores_per_chunk = attn_scores.flatten(1, 2).max(dim=2)[0].max(dim=1)[0] # [bsz, chunk]
873
+ max_scores_per_chunk = attn_scores.flatten(1, 2)
874
+
875
+ # [bsz, nhead*groups, chunk]
876
+ if self.head_reduce_method == "max":
877
+ max_scores_per_chunk = max_scores_per_chunk.max(dim=1).values
878
+ elif self.head_reduce_method == "mean":
879
+ max_scores_per_chunk = max_scores_per_chunk.mean(dim=1)
880
+ else:
881
+ raise NotImplementedError(f"Unsupported head reduce method: {self.head_reduce_method}")
882
+
883
+ if self.query_reduce_method == "max":
884
+ max_scores_per_chunk = max_scores_per_chunk.max(dim=1).values
885
+ elif self.query_reduce_method == "mean":
886
+ # 1. 调整 mask 形状以匹配 max_scores_per_chunk
887
+ # [bsz, 1, seqlen, 1] -> [bsz, seqlen, 1]
888
+ mask = query_mask.squeeze(1)
889
+
890
+ # 2. 在求和前,将无效位置(min_val)归零
891
+ # 使用 torch.where 比乘法更安全,且能有效处理极小的 min_val
892
+ masked_sum = torch.where(mask, max_scores_per_chunk, torch.zeros_like(max_scores_per_chunk)).sum(dim=1)
893
+
894
+ # 3. 计算每个 sequence 中的有效 token 数量
895
+ # 防止除以 0,使用 clamp 或加一个极小值 eps
896
+ valid_counts = mask.sum(dim=1).clamp(min=1.0)
897
+
898
+ # 4. 得到最终均值 [bsz, chunk]
899
+ max_scores_per_chunk = masked_sum / valid_counts
900
+ elif self.query_reduce_method == "last":
901
+ if q_lens is None:
902
+ q_lens = query_mask.squeeze(3).squeeze(1).sum(dim=1).long()
903
+
904
+ # 边界保护: 如果 length 为 0 (异常情况), clamp 到 0
905
+ last_indices = (q_lens - 1).clamp(min=0)
906
+
907
+ # 3. 调整形状用于 gather
908
+ # [B, 1, C]
909
+ gather_idx = last_indices.view(bsz, 1, 1).expand(-1, 1, slice_desc.nr_chunks)
910
+
911
+ # 4. 提取
912
+ max_scores_per_chunk = max_scores_per_chunk.gather(1, gather_idx).squeeze(1)
913
+ else:
914
+ raise NotImplementedError(f"Unsupported query reduce method: {self.query_reduce_method}")
915
+
916
+ # 3. Local Scatter (Chunk -> Doc)
917
+ # slice_desc.local_doc_ids_0 已经在 GPU 上预计算好了
918
+ # [1, chunk] -> [bsz, chunk]
919
+ scatter_indices = slice_desc.local_doc_ids_0.expand(bsz, -1)
920
+
921
+ local_doc_scores = torch.full((bsz, slice_desc.nr_docs), -float('inf'), device=self.device, dtype=query_states.dtype)
922
+ if self.chunk_reduce_method == "max":
923
+ local_doc_scores.scatter_reduce_(
924
+ dim=1, index=scatter_indices, src=max_scores_per_chunk,
925
+ reduce="amax", include_self=True
926
+ )
927
+
928
+ elif self.chunk_reduce_method == "mean":
929
+ doc_sums = torch.zeros_like(local_doc_scores)
930
+ doc_sums = doc_sums.scatter_reduce(
931
+ dim=1, index=scatter_indices, src=max_scores_per_chunk,
932
+ reduce="sum", include_self=False
933
+ )
934
+ doc_counts = torch.zeros_like(local_doc_scores)
935
+ ones = torch.ones_like(max_scores_per_chunk)
936
+ doc_counts = doc_counts.scatter_reduce(
937
+ dim=1, index=scatter_indices, src=ones,
938
+ reduce="sum", include_self=False
939
+ )
940
+ doc_counts = doc_counts.clamp(min=1.0)
941
+ mean_scores = doc_sums / doc_counts
942
+ # TODO: 能不能不要重新产生一个local_doc_scores?
943
+ local_doc_scores = torch.where(doc_counts > 0, mean_scores, local_doc_scores)
944
+
945
+ del doc_sums, doc_counts, mean_scores
946
+ else:
947
+ raise ValueError(f"Invalid chunk reduction method: {self.chunk_reduce_method}")
948
+
949
+ # 4. Global Scatter (Local Doc -> Global Doc)
950
+ global_indices = slice_desc.original_doc_ids.expand(bsz, -1)
951
+ global_doc_scores.scatter_reduce_(
952
+ dim=1, index=global_indices, src=local_doc_scores,
953
+ reduce="amax", include_self=True
954
+ )
955
+ del attn_scores, max_scores_per_chunk, local_doc_scores
956
+
957
+ # --- Phase 2: Top-K 选择 (使用 topk 替代 sort) ---
958
+
959
+ # 优化点:topk 比 sort 快得多
960
+ final_scores, batch_selected_doc_ids = torch.topk(global_doc_scores, k=self.model_config.doc_top_k, dim=1) # [bsz, k]
961
+
962
+ required_doc_mask = torch.zeros(total_docs, dtype=torch.bool, device=self.device)
963
+ required_doc_mask.scatter_(0, batch_selected_doc_ids.flatten(), True)
964
+
965
+ if not required_doc_mask.any():
966
+ return None, None, None, None
967
+
968
+ # --- Phase 3: 异步并行提取 (Async Retrieval) ---
969
+
970
+ block = self.blocks[layer_idx]
971
+ pooled_doc_ids = self.block_desc.pool_ids # [num_chunks]
972
+
973
+ # 计算 Chunk Mask (GPU)
974
+ required_chunk_mask = required_doc_mask[pooled_doc_ids] # [num_chunks] Bool
975
+
976
+ # 确保先处理 K
977
+ k_selected = block.k[:, :, required_chunk_mask, :]
978
+ pooled_doc_ids_selected = self.block_desc.create_global_doc_ids(pooled_doc_ids[required_chunk_mask])
979
+
980
+ cpu_indices = torch.where(required_chunk_mask)[0].to("cpu")
981
+ if rk_selected.is_cpu:
982
+ rk_selected = block.rk[:, :, cpu_indices, :]
983
+ rk_selected = rk_selected.to(self.device, non_blocking=True)
984
+ else:
985
+ rk_selected = block.rk[:, :, required_chunk_mask, :]
986
+
987
+ v_selected = block.v[:, :, cpu_indices, :].to(self.device, non_blocking=True)
988
+
989
+ # 由于 K 和 IDs 用了 non_blocking,我们需要在使用它们之前确保传输完成
990
+ # 这里的 synchronize 确保 k_retrieved 和 pooled_doc_ids_retrieved 数据已就绪
991
+ torch.cuda.current_stream().synchronize()
992
+
993
+ return final_scores, k_selected, rk_selected, v_selected, pooled_doc_ids_selected
994
+
995
+ def gpu_select(self, scores, k, rk, v, pooled_doc_ids, total_docs):
996
+ final_scores, batch_selected_doc_ids = torch.topk(scores, k=self.model_config.doc_top_k, dim=1) # [bsz, k]
997
+
998
+ required_doc_mask = torch.zeros(total_docs, dtype=torch.bool, device=self.device)
999
+ required_doc_mask.scatter_(0, batch_selected_doc_ids.flatten(), True)
1000
+
1001
+ # 计算 Chunk Mask (GPU)
1002
+ required_chunk_mask = required_doc_mask[pooled_doc_ids] # [num_chunks] Bool
1003
+
1004
+ # 确保先处理 K
1005
+ k_selected = k[:, :, required_chunk_mask, :]
1006
+ pooled_doc_ids_selected = pooled_doc_ids[required_chunk_mask]
1007
+ rk_selected = rk[:, :, required_chunk_mask, :]
1008
+ v_selected = v[:, :, required_chunk_mask, :]
1009
+
1010
+ return final_scores, k_selected, rk_selected, v_selected, pooled_doc_ids_selected
1011
+
1012
+
1013
+ class MSAService(Memory, MemoryClientBase):
1014
+ def __init__(self,
1015
+ gpu_id: int,
1016
+ generate_config: GenerateConfig,
1017
+ model_config: ModelConfig,
1018
+ memory_config: MemoryConfig
1019
+ ):
1020
+ self.world_size = generate_config.world
1021
+
1022
+ # 初始化 NCCL
1023
+ # 注意:这里假设是在单机多卡环境下运行,使用 localhost
1024
+ if "MASTER_ADDR" not in os.environ:
1025
+ os.environ["MASTER_ADDR"] = "localhost"
1026
+ if "MASTER_PORT" not in os.environ:
1027
+ os.environ["MASTER_PORT"] = "29500"
1028
+
1029
+ # 初始化进程组
1030
+ # timeout 设置稍长一点,防止初始化时某些进程慢导致超时
1031
+ dist.init_process_group(
1032
+ "nccl",
1033
+ rank=gpu_id,
1034
+ world_size=self.world_size
1035
+ )
1036
+ print(f"[GPU {gpu_id}] NCCL Initialized successfully.")
1037
+
1038
+ super().__init__(gpu_id, generate_config, model_config, memory_config)
1039
+
1040
+ self.generate_kwarg = self._create_generate_args()
1041
+
1042
+ def _create_generate_args(self):
1043
+ generate_kwarg = {
1044
+ "do_sample": True,
1045
+ "top_p": self.generate_config.top_p,
1046
+ "temperature": self.generate_config.temperature,
1047
+ "max_new_tokens": self.generate_config.max_generate_tokens,
1048
+ }
1049
+ if self.generate_config.temperature == 0.0:
1050
+ generate_kwarg["do_sample"] = False
1051
+ generate_kwarg["temperature"] = None
1052
+ generate_kwarg["top_p"] = None
1053
+ generate_kwarg["top_k"] = None
1054
+ return generate_kwarg
1055
+
1056
+
1057
+ def setup_memory_client(self, model):
1058
+ def _setup_module(module):
1059
+ if hasattr(module, 'set_memory_client') and callable(module.set_memory_client):
1060
+ try:
1061
+ module.set_memory_client(self)
1062
+ except Exception as e:
1063
+ print(f"✗ 调用 {module.__class__.__name__} 的 set_memory_client() 失败: {e}")
1064
+
1065
+ # 递归遍历所有模块
1066
+ for _, module in model.named_modules():
1067
+ _setup_module(module)
1068
+
1069
+
1070
+ def load_model(self):
1071
+ """加载模型和tokenizer"""
1072
+ # 加载tokenizer
1073
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_config.model_path)
1074
+
1075
+ # 加载模型
1076
+ self.model = MSAForCausalLM.from_pretrained(
1077
+ self.model_config.model_path,
1078
+ use_cache=True,
1079
+ attn_implementation="flash_attention_2",
1080
+ torch_dtype="auto",
1081
+ device_map=self.device,
1082
+ )
1083
+
1084
+ self.model.eval()
1085
+ self.setup_memory_client(self.model)
1086
+
1087
+
1088
+ def _gather_querys(self, query_states: torch.Tensor, query_mask: torch.Tensor):
1089
+ """处理变长 seqlen:填充到统一长度并 all_gather"""
1090
+ bsz, nhead, seqlen, hdim = query_states.shape
1091
+ num_kv_groups = self.num_key_value_groups
1092
+ num_kv_heads = nhead // num_kv_groups
1093
+ world_size = self.world_size
1094
+ device = self.device
1095
+ if self.scaling > 0:
1096
+ query_states = query_states * self.scaling
1097
+
1098
+ # --- Step 1: 准备全量 Query (分布式 All-Gather) ---
1099
+ m_for_search = query_mask.view(bsz, 1, 1, seqlen, 1)
1100
+ q_for_search = query_states.view(bsz, num_kv_heads, num_kv_groups, seqlen, hdim)
1101
+
1102
+ # --- Step 2: 处理变长 Seqlen (隐式同步处理) ---
1103
+ local_seqlen = torch.tensor([seqlen], device=device, dtype=torch.long)
1104
+ all_seqlens = [torch.empty(1, device=device, dtype=torch.long) for _ in range(world_size)]
1105
+ dist.all_gather(all_seqlens, local_seqlen)
1106
+
1107
+ # 此处 s.item() 确实会触发同步,但这是为了在 CPU 端分配 all_queries 的 List
1108
+ seqlens = [s.item() for s in all_seqlens]
1109
+ max_seqlen = max(seqlens)
1110
+
1111
+ # --- Step 3: 预分配连续内存进行 All-Gather (解决 Contiguous 问题) ---
1112
+ # 我们预先分配一个大的连续 Buffer,然后把 local 数据 copy 进去
1113
+ # 这样 gather 出来的数据天生就是连续的,不需要调 .contiguous()
1114
+ def get_padded_gather_list(tensor, target_seqlen):
1115
+ # 构造 padded 形状
1116
+ padded_shape = list(tensor.shape)
1117
+ padded_shape[3] = target_seqlen # 修改 seqlen 维度
1118
+
1119
+ # 分配全量列表
1120
+ gather_list = [torch.zeros(padded_shape, dtype=tensor.dtype, device=device) for _ in range(world_size)]
1121
+
1122
+ # 将本地数据 copy 到 gather_list 中对应的位置
1123
+ # 这里 slice 操作 + copy_ 比先 pad 再 gather 更节省临时显存
1124
+ gather_list[self.gpu_id][:, :, :, :seqlen, :].copy_(tensor)
1125
+ dist.all_gather(gather_list, gather_list[self.gpu_id])
1126
+ return gather_list
1127
+
1128
+ all_queries = get_padded_gather_list(q_for_search, max_seqlen)
1129
+ all_masks = get_padded_gather_list(m_for_search, max_seqlen)
1130
+
1131
+ return all_queries, all_masks, seqlens
1132
+
1133
+ def doc_query(self, query_states: torch.Tensor, query_mask: torch.Tensor, layer_idx: int):
1134
+ """
1135
+ 集成版:检索 + 跨卡提取 + 查表加速 + 结果归集
1136
+ 返回:
1137
+ final_k, final_v: [H, Total_Selected_C, D] 格式,直接用于 Flash Attn 散射
1138
+ final_scores: [B, TopK] 文档检索分数
1139
+ num_selected_chunks_per_sample: [B] 每个 Batch 选中的 chunk 总数
1140
+ """
1141
+ world_size = self.world_size
1142
+ bsz, nhead, seqlen, hdim = query_states.shape
1143
+ num_kv_groups = self.num_key_value_groups
1144
+ num_kv_heads = nhead // num_kv_groups
1145
+ device = self.device
1146
+ dtype = query_states.dtype
1147
+ top_k = self.model_config.doc_top_k
1148
+
1149
+ all_queries, all_masks, seqlens = self._gather_querys(query_states, query_mask)
1150
+
1151
+ # --- Step 2: 循环处理各个 Rank 的请求并本地打分 ---
1152
+ scores_list, combined_ids_list = [], []
1153
+ for target_rank in range(world_size):
1154
+ seqlen = seqlens[target_rank]
1155
+ q_batch = all_queries[target_rank][:, :, :seqlen, :]
1156
+ m_batch = all_masks[target_rank][:, :, :seqlen, :]
1157
+ # q_batch 来自 target_rank,我们需要在当前 GPU 上寻找最佳匹配
1158
+ l_scores, l_ids, g_ids = self.prefill_stage2(layer_idx, q_batch, m_batch)
1159
+ scores_list.append(l_scores) # [bsz, top_k]
1160
+ # 将 local_ids 和 global_ids 打包在最后一个维度,减少一次 all-to-all
1161
+ combined_ids_list.append(torch.stack([l_ids, g_ids], dim=-1))
1162
+
1163
+ # --- Step 3: 元数据交换 (All-to-All) ---
1164
+ # 发送:我为各 Rank 算的 Top-K;接收:各 Rank 为“我”算的 Top-K
1165
+ send_scores = torch.stack(scores_list, dim=0) # [world_size, bsz, top_k]
1166
+ send_combined_ids = torch.stack(combined_ids_list, dim=0) # [world_size, bsz, top_k, 2]
1167
+
1168
+ recv_scores = torch.empty_like(send_scores)
1169
+ recv_combined_ids = torch.empty_like(send_combined_ids)
1170
+
1171
+ dist.all_to_all_single(recv_scores, send_scores)
1172
+ dist.all_to_all_single(recv_combined_ids, send_combined_ids)
1173
+
1174
+ # --- Step 4: 全局 Top-K 决策 ---
1175
+ # shapes: [world_size, bsz, top_k] -> [bsz, world_size * top_k]
1176
+ candidate_scores = recv_scores.transpose(0, 1).reshape(bsz, -1)
1177
+ candidate_local_ids = recv_combined_ids[..., 0].transpose(0, 1).reshape(bsz, -1)
1178
+ candidate_global_ids = recv_combined_ids[..., 1].transpose(0, 1).reshape(bsz, -1)
1179
+
1180
+ origin_ranks = torch.arange(world_size, device=device).view(world_size, 1, 1).expand(-1, bsz, top_k) # [world_size, bsz, top_k]
1181
+ origin_ranks = origin_ranks.transpose(0, 1).reshape(bsz, -1) # [bsz, world_size * top_k]
1182
+
1183
+ # [bsz, top_k]
1184
+ final_scores, final_indices = torch.topk(candidate_scores, k=top_k, dim=1)
1185
+ winner_local_ids = candidate_local_ids.gather(1, final_indices)
1186
+ winner_global_ids = candidate_global_ids.gather(1, final_indices)
1187
+ winner_ranks = origin_ranks.gather(1, final_indices)
1188
+
1189
+ # 屏蔽掉无效分数
1190
+ valid_mask = final_scores > -1e9
1191
+ winner_local_ids = winner_local_ids.masked_fill(~valid_mask, -1)
1192
+
1193
+ # --- Step 5: 任务下发 (Request) ---
1194
+ # 告诉其他 Rank:我最终选了你哪几个 local_id
1195
+ task_mask = torch.arange(world_size, device=device).view(world_size, 1, 1)
1196
+ tasks_to_send = torch.where(winner_ranks == task_mask, winner_local_ids, -1)
1197
+ remote_tasks = torch.empty_like(tasks_to_send)
1198
+ dist.all_to_all_single(remote_tasks, tasks_to_send)
1199
+ remote_tasks_cpu = remote_tasks.to("cpu")
1200
+
1201
+ # --- Step 6: 提取 KV (核心:针对百万级数据的查表加速) ---
1202
+
1203
+ block = self.blocks[layer_idx]
1204
+ desc = self.block_desc
1205
+
1206
+ # 1. 预处理:收集每个 Rank 的请求元数据 (在 CPU 上完成,极快)
1207
+ all_rank_local_ids = []
1208
+ chunks_per_rank = [] # 记录每个 Rank 分得的 chunk 总数,用于后续拆分
1209
+ all_rank_u_lens = [] # 记录每个 Rank 选中的每个 doc 的长度
1210
+
1211
+ for r in range(world_size):
1212
+ requested_ids = remote_tasks_cpu[r]
1213
+ # 获取该 Rank 选中的唯一 local_doc_ids
1214
+ unique_req_ids = requested_ids[requested_ids != -1].unique()
1215
+ all_rank_local_ids.append(unique_req_ids)
1216
+
1217
+ if unique_req_ids.numel() > 0:
1218
+ u_lens = desc.doc_lens_cpu[unique_req_ids]
1219
+ all_rank_u_lens.append(u_lens)
1220
+ chunks_per_rank.append(u_lens.sum().item())
1221
+ else:
1222
+ all_rank_u_lens.append(torch.empty(0, dtype=torch.long))
1223
+ chunks_per_rank.append(0)
1224
+
1225
+ # 2. 构造全局批次索引 (One-shot Index Generation)
1226
+ flat_unique_ids = torch.cat(all_rank_local_ids)
1227
+ total_chunks_all_ranks = sum(chunks_per_rank)
1228
+
1229
+ # 直接在 fetch 过程中完成 K 和 V 的合并
1230
+ # 预分配用于 all-to-all 的连续显存
1231
+ # total_chunks 是当前 Rank 需要发给所有其他 Rank 的 chunk 总和
1232
+ send_kv_combined = torch.empty((total_chunks_all_ranks, 2, num_kv_heads, hdim), device=device, dtype=dtype)
1233
+
1234
+ if total_chunks_all_ranks > 0:
1235
+ # 批量获取所有选定文档的偏移和长度
1236
+ batch_u_offsets = desc.doc_offsets_cpu[flat_unique_ids]
1237
+ batch_u_lens = torch.cat(all_rank_u_lens)
1238
+
1239
+ # 向量化生成全局索引列表
1240
+ base_indices = torch.repeat_interleave(batch_u_offsets, batch_u_lens)
1241
+ inner_seq = torch.arange(total_chunks_all_ranks) - torch.repeat_interleave(
1242
+ torch.cumsum(batch_u_lens, dim=0) - batch_u_lens, batch_u_lens
1243
+ )
1244
+ all_cpu_indices = base_indices + inner_seq
1245
+ all_gpu_indices = all_cpu_indices.to(device, non_blocking=True)
1246
+
1247
+ # 3. 一次性提取并搬运 (核心性能提升点)
1248
+ # 由于 block.k/v 是 pinned,这一步是满速 DMA 异步传输
1249
+ # [1, H, C_all, D]
1250
+ def fetch(tensor, slot_idx):
1251
+ # tensor: [1, H, C, D] -> index_select -> [1, H, Sel_C, D]
1252
+ if tensor.is_cpu:
1253
+ selected = tensor.index_select(2, all_cpu_indices).to(device, non_blocking=True)
1254
+ else:
1255
+ selected = tensor.index_select(2, all_gpu_indices)
1256
+ send_kv_combined[:, slot_idx, :, :].copy_(selected.squeeze(0).transpose(0, 1), non_blocking=True)
1257
+
1258
+ fetch(block.k, 0)
1259
+ fetch(block.v, 1)
1260
+
1261
+ # 批量处理全局 Doc ID 映射
1262
+ all_g_ids_rep = desc.doc_ids_cpu[flat_unique_ids].repeat_interleave(batch_u_lens).to(device, non_blocking=True)
1263
+ else:
1264
+ # 全量空处理
1265
+ all_g_ids_rep = torch.empty(0, device=device, dtype=torch.long)
1266
+
1267
+ send_num_chunks = torch.tensor(chunks_per_rank, device=device)
1268
+
1269
+ # --- Step 7: 变长数据交换 (All-to-All) ---
1270
+ recv_num_chunks = torch.zeros_like(send_num_chunks)
1271
+ dist.all_to_all_single(recv_num_chunks, send_num_chunks)
1272
+ torch.cuda.current_stream().synchronize() # 确保 fetch(CPU) 已完成同步
1273
+
1274
+ total_recv = recv_num_chunks.sum().item()
1275
+ recv_kv_flat = torch.empty(total_recv, 2, num_kv_heads, hdim, dtype=dtype, device=device)
1276
+ recv_g_ids_flat = torch.empty(total_recv, dtype=torch.long, device=device)
1277
+
1278
+ in_splits = send_num_chunks.tolist()
1279
+ out_splits = recv_num_chunks.tolist()
1280
+
1281
+ dist.all_to_all_single(recv_kv_flat, send_kv_combined, out_splits, in_splits)
1282
+ dist.all_to_all_single(recv_g_ids_flat, all_g_ids_rep, out_splits, in_splits)
1283
+ torch.cuda.current_stream().synchronize()
1284
+
1285
+ # --- Step 8: 后期处理集成 (按 Batch 重排归集) ---
1286
+ # 构造匹配矩阵 [B, TopK, Total_Recv_Chunks]
1287
+ # 利用广播找到每个 Batch Item 选中的 chunk
1288
+ # TODO: 这里可能会爆显存,后续考虑分块处理
1289
+ match_matrix = (winner_global_ids.unsqueeze(-1) == recv_g_ids_flat.unsqueeze(0).unsqueeze(0))
1290
+ mask_per_batch = match_matrix.any(dim=1) # [B, Total_Recv_Chunks]
1291
+
1292
+ # 假设 recv_k_flat 形状为 [C, 1, H, D]
1293
+ # 1. 调整维度,使 B 和 C 排在最前面,方便 mask 索引
1294
+ # unsqueeze(0) -> [1, C, 1, H, D]
1295
+ # expand(bsz, ...) -> [bsz, C, 1, H, D]
1296
+ candidate_k_exp = recv_kv_flat[:,0:1, :, :].unsqueeze(0).expand(bsz, -1, -1, -1, -1)
1297
+ candidate_v_exp = recv_kv_flat[:,1:2, :, :].unsqueeze(0).expand(bsz, -1, -1, -1, -1)
1298
+
1299
+ # 2. 利用布尔索引提取。
1300
+ # candidate_k_exp[mask_per_batch] 会根据 [B, C] 的 True 位置提取对应的 [1, H, D]
1301
+ # 结果形状: [Total_Selected_In_Batch, 1, H, D]
1302
+ final_k_raw = candidate_k_exp[mask_per_batch]
1303
+ final_v_raw = candidate_v_exp[mask_per_batch]
1304
+
1305
+ # 3. 统计数量(这部分没问题)
1306
+ num_selected_chunks_per_sample = mask_per_batch.sum(dim=1)
1307
+
1308
+ # 4. 适配输出格式 [H, Total_Selected_C, D]
1309
+ # 先 squeeze(1) 去掉那个大小为 1 的维度 -> [Total_Selected, H, D]
1310
+ # 再 permute(1, 0, 2) 交换 Total_Selected 和 H -> [H, Total_Selected, D]
1311
+ final_k_to_scatter = final_k_raw.squeeze(1).permute(1, 0, 2).contiguous()
1312
+ final_v_to_scatter = final_v_raw.squeeze(1).permute(1, 0, 2).contiguous()
1313
+
1314
+ return final_k_to_scatter, final_v_to_scatter, final_scores, num_selected_chunks_per_sample, winner_global_ids
1315
+
1316
+ def generate(self, req: GenerateRequest) -> GenerateResponse:
1317
+ # 转换为tensor
1318
+ input_ids_tensor = torch.LongTensor(req.input_ids).to(self.device)
1319
+ attention_mask_tensor = torch.LongTensor(req.attention_mask).to(self.device)
1320
+ doc_ids_tensor = torch.LongTensor(req.doc_ids).to(self.device)
1321
+ position_ids_tensor = torch.LongTensor(req.positions).to(self.device)
1322
+
1323
+ past_key_values: CustomDynamicCache = create_cache()
1324
+ past_key_values.meta["require_recall_topk"] = req.require_recall_topk
1325
+ past_key_values.meta["qa_mode"] = self.generate_config.qa_mode
1326
+ past_key_values.meta["max_generate_tokens"] = self.generate_config.max_generate_tokens
1327
+ if self.generate_config.qa_mode:
1328
+ past_key_values.meta["tokenizer"] = self.tokenizer
1329
+ past_key_values.meta["idx_to_doc"] = self.idx_to_doc
1330
+ past_key_values.meta["pattern"] = r"\[(\d+)\]"
1331
+ past_key_values.meta["response_string"] = ['' for _ in range(len(req.input_ids))]
1332
+
1333
+
1334
+ # 准备generate kwargs
1335
+ for layer_idx in range(self.msa_model_config.num_hidden_layers):
1336
+ past_key_values.record_kwargs(layer_idx, {"stage": "prefill_stage2"})
1337
+
1338
+ generate_kwargs = self.generate_kwarg.copy()
1339
+ generate_kwargs["past_key_values"] = past_key_values
1340
+
1341
+ inputs = {
1342
+ "input_ids": input_ids_tensor,
1343
+ "attention_mask": attention_mask_tensor,
1344
+ "doc_ids": doc_ids_tensor,
1345
+ "use_cache": True,
1346
+ "position_ids": position_ids_tensor,
1347
+ }
1348
+
1349
+ with torch.no_grad():
1350
+ generated_ids = self.model.generate(
1351
+ **inputs,
1352
+ **generate_kwargs,
1353
+ )
1354
+ generated_seqs = self.tokenizer.batch_decode(generated_ids, skip_special_token=True)
1355
+ if req.require_recall_topk:
1356
+ recall_topks = {layer: args['recall_topk'] for layer, args in past_key_values.cache_kwargs.items() if 'recall_topk' in args}
1357
+ else:
1358
+ recall_topks = None
1359
+
1360
+ return GenerateResponse(msg_id=req.msg_id,
1361
+ seq_id=req.seq_id,
1362
+ gpu_id=self.gpu_id,
1363
+ generated_texts=generated_seqs,
1364
+ recall_topk=recall_topks)
1365
+
1366
+
1367
+
1368
+ class MSAEngine:
1369
+ def __init__(self,
1370
+ generate_config: GenerateConfig,
1371
+ model_config: ModelConfig,
1372
+ memory_config: MemoryConfig
1373
+ ):
1374
+
1375
+ # NCCL require global cuda devices, so wen change the device setting here
1376
+ # user should set CUDA_VISIBLE_DEVICES envionment vars if they wish to select specific devices
1377
+ generate_config.devices = list(range(torch.cuda.device_count()))
1378
+
1379
+ self.device_ids = generate_config.devices
1380
+ self.world_size = generate_config.world
1381
+ self.generate_config = generate_config
1382
+ self.model_config = model_config
1383
+ self.memory_config = memory_config
1384
+
1385
+ self.worker_processes = []
1386
+ self.worker_qs: Dict[int, mp.Queue] = {}
1387
+ self.response_queue = mp.Queue()
1388
+ self.request_queue = mp.Queue()
1389
+ self.start_workers()
1390
+
1391
+ self.msg_id = 0
1392
+ self.nr_active_req = 0
1393
+ self.limiter = RequestLimiter(4)
1394
+ self.requests: Dict[int, GenerateStub] = {} # msgid -> stub
1395
+ self.lock = threading.Lock()
1396
+ self.sync_event = threading.Event()
1397
+ self.sync_rsp = None
1398
+
1399
+ self.initialize()
1400
+
1401
+ def initialize(self):
1402
+ """初始化Memory服务"""
1403
+ print("Initializing Memory service...")
1404
+
1405
+ # 加载tokenizer
1406
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_config.model_path)
1407
+ self._prepare_template()
1408
+
1409
+ # 加载memory文件
1410
+ self._load_memory_file()
1411
+
1412
+ # 先等待所有的worker都就绪了再发送 blocks
1413
+ self._wait_ready_signal()
1414
+ self._process_buckets()
1415
+
1416
+ # 必须要在bucket处理完成后才启动线程
1417
+ self.running = True
1418
+ self.recv_rsp_thread = threading.Thread(target=self.receive_response, args=(), daemon=True)
1419
+ self.recv_rsp_thread.start()
1420
+
1421
+ self.recv_req_thread = threading.Thread(target=self.receive_request, args=(), daemon=True)
1422
+ self.recv_req_thread.start()
1423
+
1424
+ print("MSAEngine initialized")
1425
+
1426
+
1427
+ def _worker_all_gather(self, cmdname: str, timeout=600):
1428
+ collected_count = 0
1429
+ responses = []
1430
+
1431
+ while collected_count < len(self.worker_qs):
1432
+ try:
1433
+ response = self.response_queue.get(timeout=timeout)
1434
+ assert response.name == cmdname, f"worker all gather got response {response.name} expect {cmdname}"
1435
+ responses.append(response)
1436
+ collected_count += 1
1437
+
1438
+ except queue.Empty:
1439
+ continue
1440
+
1441
+ responses.sort(key=lambda x: x.gpu_id)
1442
+ return responses
1443
+
1444
+ def _wait_ready_signal(self):
1445
+ collected_count = 0
1446
+
1447
+ while collected_count < len(self.worker_qs):
1448
+ ProtocolConstants.expect(self.response_queue, MEMORY_WORKER_READY)
1449
+ collected_count += 1
1450
+
1451
+ @staticmethod
1452
+ def balanced_bucket_partition(docs: List[Document], n_buckets: int) -> List[List[Document]]:
1453
+ """
1454
+ 全局均衡划分算法:先全局分blocks,再分配到buckets,
1455
+ 这里并不需要每个 block 必须是最多max_chunk_per_block个 chunk,
1456
+ 而是根据max_chunk_per_block这个数大致决定分成多少 block(GPU 个数的倍数),然后将这些
1457
+ block 做得尽可能 chunk 均衡,最后分配给各个 GPU
1458
+
1459
+ Returns:
1460
+ List[List[Document]]: 每个 bucket 分配到的文档列表
1461
+ """
1462
+
1463
+ # 全局 blocks 数量
1464
+ bucket_docs: List[List[Document]] = [[] for _ in range(n_buckets)] # 每个 bucket 所含的 doc 列表
1465
+ bucket_chunk_count = [0] * n_buckets
1466
+
1467
+ def next_bucket():
1468
+ return bucket_chunk_count.index(min(bucket_chunk_count))
1469
+
1470
+ for doc in docs:
1471
+ bucket_idx = next_bucket()
1472
+ bucket_docs[bucket_idx].append(doc)
1473
+ bucket_chunk_count[bucket_idx] += doc.num_chunks
1474
+
1475
+
1476
+ return bucket_docs
1477
+
1478
+ def _sort_reference(self, docs: List[str]) -> Tuple[List[str], List[int], List[int], List[List[int]]]:
1479
+ """
1480
+ 对文档进行重排序并且分 bucket和block,分配到一个 GPU 上的文档被��为 bucket,
1481
+ bucket 和 bucket 之间的 chunk 数量尽可能均衡
1482
+ Args:
1483
+ docs: 文档数据列表
1484
+
1485
+ Returns:
1486
+ List[List[Document]]: 每个 bucket 分配到的文档列表
1487
+ """
1488
+
1489
+
1490
+ documents: List[Document] = []
1491
+ kernel_sz = self.model_config.pooling_kernel_size
1492
+ for idx, doc in enumerate(docs): # idx 是 block 内部的 doc 索引
1493
+ new_doc, doc_inputs = compose_input(doc, idx, self.tokenizer)
1494
+ length = len(doc_inputs["input_ids"])
1495
+ num_chunks = (length + kernel_sz - 1) // kernel_sz
1496
+ # print(f"doc {idx} str {len(doc)} id length: {length}, num_chunks: {num_chunks}")
1497
+
1498
+ documents.append(Document(doc=doc, doc_id=idx, num_chunks=num_chunks))
1499
+
1500
+ return MSAEngine.balanced_bucket_partition(documents, self.generate_config.world)
1501
+
1502
+ def _load_memory_file(self):
1503
+ """加载memory文件"""
1504
+ print(f"Loading memory file: {self.memory_config.memory_file_path}")
1505
+
1506
+ if self.memory_config.memory_file_path.endswith('.json'):
1507
+ with open(self.memory_config.memory_file_path, 'r') as f:
1508
+ data = json.load(f)
1509
+ # 处理JSON格式数据
1510
+ # 这里需要根据实际的JSON格式来实现
1511
+ context_list = data
1512
+
1513
+ elif self.memory_config.memory_file_path.endswith('.pkl'):
1514
+ with open(self.memory_config.memory_file_path, 'rb') as f:
1515
+ reference_metas = pickle.load(f)
1516
+
1517
+ context_list = list(reference_metas)
1518
+
1519
+ else:
1520
+ raise ValueError(f"Unsupported file format: {self.memory_config.memory_file_path}")
1521
+
1522
+ # debug function:
1523
+ # when env DEBUG_SCALE_MEMORY is set by MemoryFilePath:ScaleFactor
1524
+ # we will read from MemoryFilePath and scale it to (ScaleFactor+1) times of memory documents
1525
+ # if it is set by ScaleFactor, without MemoryFilePath, we will use self.memory_config.memory_file_path instead
1526
+ scale_path = os.environ.get("DEBUG_SCALE_MEMORY", "")
1527
+ if scale_path:
1528
+ g = scale_path.split(":")
1529
+ scale = 0
1530
+ if len(g) == 2:
1531
+ scale_path = g[0]
1532
+ scale=int(g[1])
1533
+ elif len(g) == 1:
1534
+ scale_path = self.memory_config.memory_file_path
1535
+ scale=int(g[0])
1536
+ else:
1537
+ print("invalid scaling, ignore: ", scale_path)
1538
+
1539
+ if scale > 0:
1540
+ print(f">>>>>>>>>>>>>>>>>>>> DEBUG: scale from query file {scale_path} with scale {scale}", )
1541
+ from src.utils.scale import scale_memory
1542
+ scale_memory(context_list, scale_path, scale=scale)
1543
+
1544
+ self.buckets: List[List[Document]] = self._sort_reference(context_list)
1545
+ self.docs: List[Document] = []
1546
+ for bucket in self.buckets:
1547
+ self.docs.extend(bucket)
1548
+ chunks = sum(doc.num_chunks for doc in self.docs)
1549
+
1550
+ print(f"Loaded {len(self.docs)} memory document, total chunks: {chunks}")
1551
+ for idx, bucket in enumerate(self.buckets):
1552
+ print(f"bucket {idx} contains {len(bucket)} documents, {sum(doc.num_chunks for doc in bucket)} chunks")
1553
+
1554
+ def _process_buckets(self):
1555
+ """
1556
+ 发送 blocks 到各 worker
1557
+ """
1558
+
1559
+ idx_to_doc = self.get_idx_to_doc()
1560
+ for idx, gpu_id in enumerate(self.generate_config.devices):
1561
+ ProtocolConstants.send(self.worker_qs[gpu_id],
1562
+ MEMORY_WORKER_BLOCKS,
1563
+ data=self.buckets[idx],
1564
+ block=False)
1565
+ ProtocolConstants.send(self.worker_qs[gpu_id],
1566
+ MEMORY_WORKER_IDX_TO_DOC,
1567
+ data=idx_to_doc,
1568
+ block=False)
1569
+
1570
+ # 拿到所有的 doc id bias然后累积更新各 worker
1571
+ rsps: List[ReportDocID] = self._worker_all_gather( "report_doc_id", timeout=None)
1572
+
1573
+ # pooled_doc_id_bias = 0
1574
+ # for rsp in rsps:
1575
+ # self.worker_qs[rsp.gpu_id].put(SetDocIDCmd(pooled_doc_id_bias=pooled_doc_id_bias))
1576
+ # pooled_doc_id_bias += rsp.pooled_doc_id_bias
1577
+ # _ = self._worker_all_gather("set_doc_id")
1578
+
1579
+ def receive_request(self):
1580
+ while self.running:
1581
+ try:
1582
+ gpu_id, res = self.request_queue.get(timeout=3)
1583
+ print(f"Received response from GPU {gpu_id}")
1584
+ except queue.Empty:
1585
+ continue
1586
+
1587
+ def get_idx_to_doc(self):
1588
+ return {doc.doc_id: doc.doc for doc in self.docs}
1589
+
1590
+ @staticmethod
1591
+ def service_main(gpu_id: int,
1592
+ request_queue: mp.Queue,
1593
+ response_queue: mp.Queue,
1594
+ generate_config: GenerateConfig,
1595
+ model_config: ModelConfig,
1596
+ memory_config: MemoryConfig
1597
+ ):
1598
+ # 1. 初始化 Service (包含 NCCL Init)
1599
+ service = MSAService(gpu_id, generate_config, model_config, memory_config)
1600
+
1601
+ print(f"MSAService-{gpu_id} is ready")
1602
+
1603
+ # notify parent I'm ready
1604
+ ProtocolConstants.send(response_queue, MEMORY_WORKER_READY, block=False)
1605
+
1606
+ # wait parent to give me blocks to process
1607
+ docs: List[Document] = ProtocolConstants.expect(request_queue, MEMORY_WORKER_BLOCKS)
1608
+ idx_to_doc: Dict[int, str] = ProtocolConstants.expect(request_queue, MEMORY_WORKER_IDX_TO_DOC)
1609
+ service.save_idx_to_doc(idx_to_doc)
1610
+
1611
+ service.generate_blocks(docs)
1612
+ del docs
1613
+ torch.cuda.empty_cache()
1614
+
1615
+ service.load_model()
1616
+
1617
+ # tell Memory my doc id bias
1618
+ response_queue.put(ReportDocID(gpu_id=gpu_id, pooled_doc_id_bias=service.get_max_local_pool_doc_id()), block=False)
1619
+
1620
+
1621
+ while True:
1622
+ try:
1623
+
1624
+ cmd: CmdBase = request_queue.get()
1625
+ # 检查是否是终止信号
1626
+ if cmd is None:
1627
+ if dist.is_initialized():
1628
+ dist.destroy_process_group()
1629
+ return
1630
+
1631
+ elif cmd.name == "generate_request":
1632
+ rsp = service.generate(cmd)
1633
+ response_queue.put(rsp)
1634
+ # torch.cuda.empty_cache() # make perf worse
1635
+
1636
+ else:
1637
+ print(f"GPU_ID {gpu_id} recv unknown cmd {cmd.name}")
1638
+
1639
+ except Exception as e:
1640
+ print(f"[subprocess {gpu_id}] error: {e}")
1641
+ import traceback
1642
+ traceback.print_exc()
1643
+
1644
+
1645
+ def __enter__(self):
1646
+ return self
1647
+
1648
+ def __exit__(self, exc_type, exc_val, exc_tb):
1649
+ self.stop_workers()
1650
+ return False # 不抑制异常
1651
+
1652
+
1653
+ def start_workers(self):
1654
+ # 设置启动方式,CUDA 必须用 spawn
1655
+ mp.set_start_method('spawn', force=True)
1656
+
1657
+ for gpu_id in self.generate_config.devices:
1658
+ request_queue = mp.Queue()
1659
+ process = mp.Process(
1660
+ target=MSAEngine.service_main,
1661
+ args=(gpu_id, request_queue, self.response_queue, self.generate_config, self.model_config, self.memory_config),
1662
+ name=f"MSAService-{gpu_id}"
1663
+ )
1664
+ process.start()
1665
+ self.worker_processes.append(process)
1666
+ self.worker_qs[gpu_id] = request_queue
1667
+
1668
+ def stop_workers(self):
1669
+ print("MSAEngine stop all services")
1670
+ self.running = False
1671
+
1672
+ for q in self.worker_qs.values():
1673
+ q.put(None)
1674
+
1675
+ self.worker_qs = {}
1676
+
1677
+ # 等待所有worker退出
1678
+ for process in self.worker_processes:
1679
+ process.join()
1680
+ self.worker_processes = []
1681
+
1682
+ self.recv_rsp_thread.join()
1683
+ self.recv_rsp_thread = None
1684
+
1685
+ self.recv_req_thread.join()
1686
+ self.recv_req_thread = None
1687
+
1688
+ def _validate_inputs(self, input_ids: List[List[int]]):
1689
+ if any(len(sub) == 0 for sub in input_ids):
1690
+ raise ValueError(f"empty query is not acceptable")
1691
+
1692
+ if self.generate_config.max_seq_len > 0:
1693
+ seqlen = sum(sum(sub) for sub in input_ids)
1694
+ if seqlen > self.generate_config.max_seq_len:
1695
+ raise ValueError(f"total sequence length {seqlen} exceeds limitation {self.generate_config.max_seq_len}")
1696
+
1697
+ if self.generate_config.max_query_seq_len > 0:
1698
+ max_seq_len = max(sum(sub) for sub in input_ids)
1699
+ if max_seq_len > self.generate_config.max_query_seq_len:
1700
+ raise ValueError(f"max query sequence length {max_seq_len} exceeds limitation {self.generate_config.max_query_seq_len}")
1701
+
1702
+ def _prepare_template(self):
1703
+ response_head = "<|im_start|>"
1704
+ response_head_inputs = self.tokenizer(response_head, add_special_tokens=False)
1705
+ response_head_input_ids = response_head_inputs["input_ids"]
1706
+ response_head_attention_mask = response_head_inputs["attention_mask"]
1707
+
1708
+ pad_token = self.tokenizer.pad_token
1709
+ pad_token_id = self.tokenizer.pad_token_id
1710
+ prompt_template = self.generate_config.template["prompt"].replace("{prompt}", pad_token)
1711
+ prompt_template_inputs = self.tokenizer(prompt_template, add_special_tokens=False)
1712
+ prompt_template_input_ids = prompt_template_inputs["input_ids"]
1713
+ prompt_template_attention_mask = prompt_template_inputs["attention_mask"]
1714
+ pad_index = prompt_template_input_ids.index(pad_token_id)
1715
+ template_tail_input_ids = prompt_template_input_ids[pad_index+1:]
1716
+ template_tail_attention_mask = prompt_template_attention_mask[pad_index+1:]
1717
+
1718
+ self.prompt_tail_input_ids = template_tail_input_ids + response_head_input_ids
1719
+ self.prompt_tail_attention_mask = template_tail_attention_mask + response_head_attention_mask
1720
+ self.tail_doc_ids = [-2] * (len(template_tail_input_ids)) + [-1] * len(response_head_input_ids)
1721
+
1722
+ def _apply_template(self, prompt):
1723
+ # question = "\n请根据以上历史文档信息,回答问题\n\n" + prompt + "\n" + f"请返回与问题有关的所有文档\n"
1724
+ # question = "\n请根据以上历史文档信息,回答问题\n\n" + prompt + "\n" + f"请返回与问题有关的1个文档\n"
1725
+ question = "\nPlease answer the question based on the above historical document information\n\n" + prompt + "\n" + f"Please return all documents related to the question\n"
1726
+ prompt_inputs = self.tokenizer(question, add_special_tokens=False)
1727
+ prompt_inputs["input_ids"], prompt_inputs["attention_mask"]
1728
+
1729
+ input_ids = prompt_inputs["input_ids"] + self.prompt_tail_input_ids
1730
+ attention_mask = prompt_inputs["attention_mask"] + self.prompt_tail_attention_mask
1731
+ doc_ids = [0] * (len(prompt_inputs["input_ids"])) + self.tail_doc_ids
1732
+ return input_ids, attention_mask, doc_ids
1733
+
1734
+ def _apply_template_regenerate(self, prompt):
1735
+ question = prompt.split("<regenerate>")[1]
1736
+
1737
+ prompt_inputs = self.tokenizer(question, add_special_tokens=False)
1738
+ prompt_inputs["input_ids"], prompt_inputs["attention_mask"]
1739
+
1740
+ input_ids = prompt_inputs["input_ids"]
1741
+ attention_mask = prompt_inputs["attention_mask"]
1742
+
1743
+ # Build doc_ids: 0 for specific regions, -1 for the rest
1744
+ doc_ids = [-1] * len(prompt_inputs["input_ids"])
1745
+
1746
+ # Region 1: tokens between the last "]<|object_ref_end|>[" and the first "<|object_ref_end|>" after it
1747
+ last_marker_start = question.rfind("]<|object_ref_end|>[")
1748
+ if last_marker_start != -1:
1749
+ search_start = last_marker_start + len("]<|object_ref_end|>[") - 1
1750
+ next_marker = question.find("<|object_ref_end|>", search_start)
1751
+ if next_marker != -1:
1752
+ region1_text = question[search_start:next_marker]
1753
+ region1_tokens = self.tokenizer(region1_text, add_special_tokens=False)["input_ids"]
1754
+ prefix_before_region1 = question[:search_start]
1755
+ prefix_tokens_len = len(self.tokenizer(prefix_before_region1, add_special_tokens=False)["input_ids"])
1756
+ for i in range(prefix_tokens_len, prefix_tokens_len + len(region1_tokens)):
1757
+ if i < len(doc_ids):
1758
+ doc_ids[i] = 0
1759
+
1760
+ # Region 2: "Please return all documents related to the question" and everything before it
1761
+ anchor = "Please return all documents related to the question"
1762
+ anchor_pos = question.find(anchor)
1763
+ if anchor_pos != -1:
1764
+ prefix_with_anchor = question[:anchor_pos + len(anchor)]
1765
+ prefix_token_len = len(self.tokenizer(prefix_with_anchor, add_special_tokens=False)["input_ids"])
1766
+ for i in range(prefix_token_len):
1767
+ if i < len(doc_ids):
1768
+ doc_ids[i] = 0
1769
+
1770
+ return input_ids, attention_mask, doc_ids
1771
+
1772
+ def receive_response(self):
1773
+ while self.running:
1774
+ try:
1775
+ rsp: GenerateResponse = self.response_queue.get(timeout=3)
1776
+
1777
+ final_stub = None
1778
+ with self.limiter.lock:
1779
+ assert rsp.msg_id in self.requests
1780
+ stub = self.requests[rsp.msg_id]
1781
+ stub.responses.append(rsp)
1782
+ # print(f"Request {rsp.msg_id} got {len(stub.responses)} responses")
1783
+ if len(stub.responses) == self.world_size:
1784
+ final_stub = self.requests.pop(rsp.msg_id)
1785
+
1786
+ if final_stub:
1787
+ # print("Release")
1788
+ self.limiter.release()
1789
+ final_stub.respond()
1790
+
1791
+ except queue.Empty:
1792
+ continue
1793
+
1794
+ def default_callback(self, texts: List[str], recall_topk, userdata):
1795
+ self.sync_rsp = (texts, recall_topk, userdata)
1796
+ self.sync_event.set()
1797
+
1798
+ def generate(self,
1799
+ prompts: Union[str, List[str]],
1800
+ userdata=None,
1801
+ require_recall_topk=False,
1802
+ callback: GenerateReceiver=None):
1803
+
1804
+ if isinstance(prompts, str):
1805
+ prompts = [prompts]
1806
+
1807
+ world = self.generate_config.world
1808
+ bsz = (len(prompts) + world - 1) // world
1809
+ if self.generate_config.max_batch_size > 0 and bsz > self.generate_config.max_batch_size:
1810
+ raise ValueError(f"Batch size exceeds maximum allowed batch size {self.generate_config.max_batch_size}")
1811
+ # Important:
1812
+ # 如果输入的 prompts 不够 world size,则需要 pad,我们的方法是用最后一个prompt 填充
1813
+ pads = bsz * world - len(prompts)
1814
+ if pads > 0:
1815
+ prompts += [prompts[-1]] * pads
1816
+
1817
+ batch_input_ids = []
1818
+ batch_attention_mask = []
1819
+ batch_doc_ids = []
1820
+ batch_positions = []
1821
+
1822
+ # template + pooled docs
1823
+ current_position = 3 + self.model_config.doc_top_k
1824
+
1825
+ for prompt in prompts:
1826
+ if "<regenerate>" in prompt:
1827
+ input_ids, attention_mask, doc_ids = self._apply_template_regenerate(prompt)
1828
+ else:
1829
+ input_ids, attention_mask, doc_ids = self._apply_template(prompt)
1830
+ batch_input_ids.append(input_ids)
1831
+ batch_attention_mask.append(attention_mask)
1832
+ batch_doc_ids.append(doc_ids)
1833
+
1834
+ # 计算position ids
1835
+ position_ids = [current_position + i for i in range(len(input_ids))]
1836
+ batch_positions.append(position_ids)
1837
+ # current_position += len(input_ids) # 为什么要累计长度?
1838
+
1839
+ # Pad sequences to the same length
1840
+ padded_input_ids = []
1841
+ padded_attention_mask = []
1842
+ padded_doc_ids = []
1843
+ padded_position_ids = []
1844
+
1845
+ pad_token_id = self.tokenizer.pad_token_id if self.tokenizer.pad_token_id else self.tokenizer.eos_token_id
1846
+ # 使用 doc_end_id 作为 doc_ids 的 padding 值
1847
+ # doc_end_id = self.tokenizer("<|im_end|>", add_special_tokens=False)["input_ids"][0]
1848
+
1849
+ for i in range(len(batch_input_ids)):
1850
+ if i % bsz == 0:
1851
+ max_length = max(len(ids) for ids in batch_input_ids[i:i+bsz])
1852
+ input_ids = batch_input_ids[i]
1853
+ attention_mask = batch_attention_mask[i]
1854
+ doc_ids_seq = batch_doc_ids[i]
1855
+ position_ids = batch_positions[i]
1856
+
1857
+ # Pad to max_length
1858
+ pad_length = max_length - len(input_ids)
1859
+ # 改成left pad,方便position_ids迭代,可直接与自定义的generate兼容
1860
+ if pad_length > 0:
1861
+ input_ids = [pad_token_id] * pad_length + input_ids
1862
+ attention_mask = [0] * pad_length + attention_mask
1863
+ doc_ids_seq = [0] * pad_length + doc_ids_seq
1864
+ position_ids = [position_ids[0]] * pad_length + position_ids
1865
+
1866
+ padded_input_ids.append(input_ids)
1867
+ padded_attention_mask.append(attention_mask)
1868
+ padded_doc_ids.append(doc_ids_seq)
1869
+ padded_position_ids.append(position_ids)
1870
+
1871
+ # 只检查每个 batch 的第一个 query 就好
1872
+ self._validate_inputs(padded_input_ids[0:-1:bsz])
1873
+
1874
+
1875
+ # TODO: when callback is None
1876
+ self.limiter.acquire()
1877
+
1878
+ sync = callback is None
1879
+ if callback is None:
1880
+ callback = self.default_callback
1881
+
1882
+ self.msg_id += 1
1883
+ with self.limiter.lock:
1884
+ self.requests[self.msg_id] = GenerateStub(msg_id=self.msg_id,
1885
+ userdata=userdata,
1886
+ nr_dummy=pads,
1887
+ responses=[],
1888
+ callback=callback)
1889
+
1890
+ for idx, gpu_id in enumerate(self.generate_config.devices):
1891
+ start, end = idx * bsz, (idx+1) * bsz
1892
+ req = GenerateRequest(msg_id=self.msg_id,
1893
+ seq_id=idx,
1894
+ prompts=prompts[start:end],
1895
+ input_ids=padded_input_ids[start:end],
1896
+ attention_mask=padded_attention_mask[start:end],
1897
+ doc_ids=padded_doc_ids[start:end],
1898
+ positions=padded_position_ids[start:end],
1899
+ require_recall_topk=require_recall_topk)
1900
+ # print(f"send GPU{gpu_id} batch {len(req.input_ids)}")
1901
+ self.worker_qs[gpu_id].put(req, block=False)
1902
+
1903
+ if sync:
1904
+ self.sync_event.wait()
1905
+ self.sync_event.clear()
1906
+ return self.sync_rsp
1907
+
1908
+
1909
+
1910
+
1911
+
src/prefill.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import os
3
+ from typing import List, Dict
4
+ from dataclasses import dataclass
5
+ import multiprocessing as mp
6
+ import pathlib
7
+ import torch
8
+
9
+ from transformers import AutoTokenizer, BitsAndBytesConfig
10
+
11
+ project_path = pathlib.Path(__file__).parent.parent
12
+ sys.path.append(str(project_path))
13
+
14
+ from src.utils.gpu_worker import GpuWorker
15
+ from src.utils.template import QWEN3_TEMPLATE, QWEN3_INSTRUCT_TEMPLATE
16
+ from src.utils.cache import copy_kv_cache_to_device, CustomDynamicCacheOnCPU
17
+ from src.msa.model import MSAForCausalLM
18
+ from src.utils.gpu_worker import GpuWorker
19
+ from src.utils.tools import compose_input
20
+ from src.types import Document, ProtocolConstants
21
+
22
+ @dataclass
23
+ class BlockModelInput:
24
+ doc_input_ids: torch.Tensor
25
+ doc_attention_mask: torch.Tensor
26
+ doc_ids: torch.Tensor
27
+ position_ids: torch.Tensor
28
+ num_chunks: int
29
+ chunk_sizes: List[int]
30
+
31
+ PREFILL_WORKER_READY = "PREFILL_WORKER_READY"
32
+ PREFILL_WORKER_CLOSE = "PREFILL_WORKER_CLOSE"
33
+ PREFILL_WORKER_MEMORY_DOCS = "PREFILL_WORKER_MEMORY_DOCS"
34
+ PREFILL_WORKER_NUM_CHUNKS_REPORT = "PREFILL_WORKER_NUM_CHUNKS_REPORT"
35
+ PREFILL_WORKER_META = "PREFILL_WORKER_META"
36
+
37
+ class PrefillStage1Worker(GpuWorker):
38
+ """Memory工作进程"""
39
+
40
+ def __init__(self, gpu_id: int, model_path: str, template: dict,
41
+ pooling_kernel_size: int, envs: dict):
42
+ """
43
+ 该 worker 被MemoryWorker创建并仅执行prefill stage 1获取block 的kv cache
44
+ """
45
+ super().__init__(gpu_id, envs)
46
+ self.model_path = model_path
47
+ self.pooling_kernel_size = pooling_kernel_size
48
+
49
+ self._load_model()
50
+ self.model_config = self.model.config
51
+
52
+ self.template_id = -2
53
+ self._prepare_template(template)
54
+
55
+ def num_model_layers(self):
56
+ return self.model_config.num_hidden_layers
57
+
58
+ def _load_model(self):
59
+ """加载模型和tokenizer"""
60
+ # 加载tokenizer
61
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_path)
62
+
63
+ # 加载模型
64
+ self.model = MSAForCausalLM.from_pretrained(
65
+ self.model_path,
66
+ use_cache=True,
67
+ attn_implementation="flash_attention_2",
68
+ torch_dtype="bfloat16",
69
+ device_map=self.device,
70
+ )
71
+ self.model.eval()
72
+
73
+
74
+ @staticmethod
75
+ def split_docs(docs: List[Document], block_size: int):
76
+ sub_blocks = []
77
+ curr_block = []
78
+ sz = 0
79
+ for doc in docs:
80
+ chunks = doc.num_chunks
81
+ if sz + chunks > block_size and curr_block:
82
+ sub_blocks.append(curr_block)
83
+ sz = 0
84
+ curr_block = []
85
+ curr_block.append(doc)
86
+ sz += chunks
87
+
88
+ if curr_block:
89
+ sub_blocks.append(curr_block)
90
+
91
+ return sub_blocks
92
+
93
+ @staticmethod
94
+ def wait_for_ready(q: mp.Queue):
95
+ ProtocolConstants.expect(q, PREFILL_WORKER_READY)
96
+
97
+ @staticmethod
98
+ def close_worker(q: mp.Queue):
99
+ ProtocolConstants.send(q, PREFILL_WORKER_CLOSE, block=True)
100
+
101
+ @staticmethod
102
+ def send_documents(q: mp.Queue, docs):
103
+ ProtocolConstants.send(q,
104
+ PREFILL_WORKER_MEMORY_DOCS,
105
+ data=docs,
106
+ block=False)
107
+ @staticmethod
108
+ def recv_meta(q: mp.Queue):
109
+ return ProtocolConstants.expect(q, PREFILL_WORKER_META)
110
+
111
+ @staticmethod
112
+ def prefill_worker_main(gpu_id: int, request_queue: mp.Queue, response_queue: mp.Queue,
113
+ model_path: str, template: Dict,
114
+ pooling_kernel_size: int, block_size: int, envs):
115
+
116
+ # print(f"prefill worker {gpu_id} started")
117
+ worker = PrefillStage1Worker(gpu_id, model_path, template, pooling_kernel_size, envs)
118
+
119
+ # notify parent I'm ready
120
+ ProtocolConstants.send(response_queue, PREFILL_WORKER_READY, block=False)
121
+
122
+ docs: List[Document] = ProtocolConstants.expect(request_queue, PREFILL_WORKER_MEMORY_DOCS)
123
+ try:
124
+ for block in PrefillStage1Worker.split_docs(docs, block_size):
125
+ meta = worker.inference(block)
126
+
127
+ # send to master worker process and continue, DO NOT block
128
+ ProtocolConstants.send(response_queue,
129
+ PREFILL_WORKER_META,
130
+ data=meta,
131
+ block=False)
132
+
133
+ except Exception as e:
134
+ print(f"[子进程 {gpu_id}] 发生错误: {e}")
135
+ import traceback
136
+ traceback.print_exc()
137
+
138
+ # wait for exit signal
139
+ ProtocolConstants.expect(request_queue, PREFILL_WORKER_CLOSE)
140
+ print(f"prefill worker {gpu_id} ended")
141
+
142
+ def inference(self, block: List[Document]):
143
+ """
144
+ 处理memory block
145
+
146
+ Args:
147
+ memory_block: 分配给此GPU的memory block
148
+
149
+ """
150
+ model_input = self._prepare_block_inputs(block)
151
+
152
+ kv_meta = self._inference(model_input)
153
+ kv_meta['nr_docs'] = len(block)
154
+ kv_meta['doc_ids'] = [item.doc_id for item in block]
155
+ kv_meta['nr_chunks'] = [item.num_chunks for item in block]
156
+
157
+ return kv_meta
158
+
159
+ def _prepare_template(self, template: Dict) -> Dict:
160
+ """
161
+ 重新加载单个memory block
162
+ 完整复制eval_anything_v2_batch.py中reload_memory的逻辑
163
+
164
+ Args:
165
+ block: memory block数据 [(doc_id, doc_str), ...]
166
+ template: 模板字典
167
+
168
+ Returns:
169
+ KV cache元数据
170
+ """
171
+ # 获取模板信息
172
+ self.pad_token = self.tokenizer.pad_token
173
+ self.pad_token_id = self.tokenizer.pad_token_id
174
+ self.doc_end_id = self.tokenizer("<|im_end|>", add_special_tokens=False)["input_ids"]
175
+
176
+ prompt_template = template["prompt"].replace("{prompt}", self.pad_token)
177
+ prompt_template_inputs = self.tokenizer(prompt_template, add_special_tokens=False)
178
+ self.prompt_template_input_ids = prompt_template_inputs["input_ids"]
179
+ self.prompt_template_attention_mask = prompt_template_inputs["attention_mask"]
180
+
181
+ self.pad_index = self.prompt_template_input_ids.index(self.pad_token_id)
182
+ self.template_head_input_ids = self.prompt_template_input_ids[:self.pad_index]
183
+ self.template_head_attention_mask = self.prompt_template_attention_mask[:self.pad_index]
184
+ self.template_tail_input_ids = self.prompt_template_input_ids[self.pad_index+1:]
185
+ self.template_tail_attention_mask = self.prompt_template_attention_mask[self.pad_index+1:]
186
+
187
+
188
+ def _prepare_block_inputs(self, block: List[Document]) -> Dict:
189
+ """
190
+ 重新加载单个memory block
191
+ 完整复制eval_anything_v2_batch.py中reload_memory的逻辑
192
+
193
+ Args:
194
+ block: memory block数据 [(doc_id, doc_str), ...]
195
+ template: 模板字典
196
+
197
+ Returns:
198
+ KV cache元数据
199
+ """
200
+
201
+ # 准备文档数据
202
+
203
+ # block starts with template head
204
+ doc_ids = [self.template_id] * len(self.template_head_input_ids)
205
+ doc_input_ids = [i for i in self.template_head_input_ids ]
206
+ doc_attention_mask = [i for i in self.template_head_attention_mask]
207
+ position_ids = [i for i in range(self.pad_index)]
208
+
209
+ chunk_sizes = [] # 记录每一份文档占用的 chunk 数量
210
+
211
+ for doc_idx, item in enumerate(block):
212
+ doc_id, doc, pre_calculated_num_chunk = item.doc_id, item.doc, item.num_chunks
213
+ new_doc, doc_inputs = compose_input(doc, doc_id, self.tokenizer)
214
+ # print(f"inference {doc_id+1}: {new_doc}")
215
+ # 此处必须使用 1 起始的doc_idx,因为此 id 是用于生成 pool doc ID 的
216
+ # 注意不可以使用doc_id,doc_id只能用于嵌入在语料中,使得 generate 时能生成出来,
217
+ # 而pool doc ID的作用却是用于标注产生的 kv cache chunks 和文档的对应关系
218
+ # 所以每次 stage1 的推理doc id 都是从 1 开始的
219
+ temp_doc_ids = [doc_idx+1] * len(doc_inputs["input_ids"])
220
+ temp_doc_input_ids = doc_inputs["input_ids"]
221
+ temp_doc_attention_mask = doc_inputs["attention_mask"]
222
+ length = len(temp_doc_input_ids)
223
+ temp_position_ids = [i for i in range(length)]
224
+
225
+ chunk_size = (len(temp_doc_ids) + self.pooling_kernel_size - 1) // self.pooling_kernel_size
226
+ chunk_sizes.append(chunk_size)
227
+ assert chunk_size == pre_calculated_num_chunk, f"pre calculated chunk {pre_calculated_num_chunk} got {chunk_size}, doc str {len(doc)} id len {length}/{len(temp_doc_ids)}: [{doc_id}] <{doc}>"
228
+
229
+
230
+
231
+ doc_ids.extend(temp_doc_ids)
232
+ doc_input_ids.extend(temp_doc_input_ids)
233
+ doc_attention_mask.extend(temp_doc_attention_mask)
234
+ position_ids.extend(temp_position_ids)
235
+
236
+ input_ids_tensor = torch.LongTensor([doc_input_ids])
237
+ attention_mask_tensor = torch.LongTensor([doc_attention_mask])
238
+ doc_ids_tensor = torch.LongTensor([doc_ids])
239
+ position_ids_tensor = torch.LongTensor([position_ids])
240
+
241
+ return BlockModelInput(doc_input_ids=input_ids_tensor,
242
+ doc_attention_mask=attention_mask_tensor,
243
+ doc_ids=doc_ids_tensor,
244
+ position_ids=position_ids_tensor,
245
+ num_chunks=sum(chunk_sizes),
246
+ chunk_sizes=chunk_sizes)
247
+
248
+ def _inference(self, model_input: BlockModelInput) -> Dict:
249
+ """
250
+ 重新加载单个memory block
251
+ 完整复制eval_anything_v2_batch.py中reload_memory的逻辑
252
+
253
+ Args:
254
+ block: memory block数据 [(doc_id, doc_str), ...]
255
+ template: 模板字典
256
+
257
+ Returns:
258
+ KV cache元数据
259
+ """
260
+
261
+ # 转换为tensor
262
+ input_ids_tensor = model_input.doc_input_ids.to(self.device)
263
+ attention_mask_tensor = model_input.doc_attention_mask.to(self.device)
264
+ doc_ids_tensor = model_input.doc_ids.to(self.device)
265
+ position_ids_tensor = model_input.position_ids.to(self.device)
266
+
267
+ # 创建past_key_values,这里我们会直接将 kvcache 和其他的 tensor 全部保存在 cpu 上
268
+ past_key_values = CustomDynamicCacheOnCPU()
269
+ for layer_idx in range(self.num_model_layers()):
270
+ past_key_values.record_kwargs(layer_idx, {"stage": "prefill_stage1"})
271
+
272
+ # 执行prefill
273
+ # TODO: 多 batch 会更快
274
+ with torch.no_grad():
275
+ if True:
276
+ """我们的数据太大了,会导致model lm_head产生大量的显存堆积,所以直接用model.model避过去"""
277
+ outputs = self.model.model(
278
+ input_ids=input_ids_tensor,
279
+ attention_mask=attention_mask_tensor,
280
+ position_ids=position_ids_tensor,
281
+ past_key_values=past_key_values,
282
+ use_cache=True,
283
+ output_attentions=False,
284
+ output_hidden_states=False,
285
+ output_docs_score=False,
286
+ doc_ids=doc_ids_tensor,
287
+ )
288
+ else:
289
+ outputs = self.model(
290
+ input_ids=input_ids_tensor,
291
+ attention_mask=attention_mask_tensor,
292
+ doc_ids=doc_ids_tensor,
293
+ use_cache=True,
294
+ position_ids=position_ids_tensor,
295
+ past_key_values=past_key_values,
296
+ )
297
+
298
+ torch.cuda.empty_cache()
299
+ # 构建返回的元数据
300
+ kvcache_meta = {
301
+ "chunk_sizes": model_input.chunk_sizes,
302
+ "past_key_values": outputs.past_key_values,
303
+ }
304
+
305
+ return kvcache_meta
306
+
src/types.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ import multiprocessing as mp
3
+
4
+
5
+ @dataclass
6
+ class Document:
7
+ doc: str = ""
8
+ doc_id: int = 0
9
+ num_chunks: int = 0
10
+
11
+ class ProtocolConstants:
12
+
13
+ @staticmethod
14
+ def expect(q: mp.Queue, constant):
15
+ k, v = q.get()
16
+ assert k == constant, f"expect {constant} but got {k}"
17
+ return v
18
+
19
+ @staticmethod
20
+ def send(q: mp.Queue, constant, data=None, block=True):
21
+ q.put((constant, data), block=block)
src/utils/__init__.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .common import (
2
+ patch_transformer_logging,
3
+ print_trainable_params,
4
+ draw_bounding_boxes,
5
+ post_process_generate_ids,
6
+ decode_generate_ids,
7
+ smart_tokenizer_and_embedding_resize,
8
+ )
9
+ from .callbacks import (
10
+ ModeltimeCallback,
11
+ ProfCallback,
12
+ SacredCallback,
13
+ ModelEvalCallback,
14
+ DSEmptyCacheCallback
15
+ )
src/utils/cache.py ADDED
@@ -0,0 +1,512 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ 缓存模块
4
+ 包含CustomDynamicCache和CustomQuantizeDynamicCache两个类
5
+ 从eval_anything_v2_batch.py中提取出来,实现模块化管理
6
+ """
7
+
8
+ import copy
9
+ from typing import Optional, Tuple
10
+ import torch
11
+ from transformers.cache_utils import DynamicCache, QuantoQuantizedCache, QuantizedCacheConfig
12
+
13
+
14
+ class CustomDynamicCache(DynamicCache):
15
+ """
16
+ 自定义动态缓存类
17
+ 扩展标准的DynamicCache,添加额外的元数据存储和查询功能
18
+ """
19
+ def __init__(self, _distributed_cache_data=None):
20
+ super().__init__(_distributed_cache_data)
21
+ self.cache_kwargs = {}
22
+ self.group_cache = {}
23
+ self.meta = {}
24
+ self.router_key_cache = []
25
+
26
+ def clear_kvcache(self):
27
+ self.key_cache = []
28
+ self.value_cache = []
29
+
30
+ def record_kwargs(self, layer_idx, kwargs):
31
+ """
32
+ 记录层的元数据信息
33
+
34
+ Args:
35
+ layer_idx: 层索引
36
+ kwargs: 包含路由层信息的字典
37
+ """
38
+ if layer_idx in self.cache_kwargs:
39
+ self.cache_kwargs[layer_idx].update(kwargs)
40
+ else:
41
+ self.cache_kwargs[layer_idx] = kwargs
42
+
43
+ def get_layer_length(self):
44
+ return len(self.cache_kwargs)
45
+
46
+ def get_kvcache(self, layer_idx):
47
+ """
48
+ 获取指定层的KV缓存
49
+
50
+ Args:
51
+ layer_idx: 层索引
52
+
53
+ Returns:
54
+ Tuple[torch.Tensor, torch.Tensor]: (key_cache, value_cache)
55
+ """
56
+ key_cache = self.key_cache[layer_idx]
57
+ value_cache = self.value_cache[layer_idx]
58
+ return key_cache, value_cache
59
+
60
+ def get_router_kcache(self, layer_idx):
61
+ if layer_idx < len(self.router_key_cache):
62
+ return self.router_key_cache[layer_idx]
63
+ else:
64
+ return None
65
+
66
+ def clear_query(self):
67
+ """
68
+ 清理查询相关的临时数据
69
+ 移除查询过程中产生的临时数据,保持缓存清洁
70
+ """
71
+ for k, v in self.cache_kwargs.items():
72
+ if "compacked_key_cache" in v:
73
+ v.pop("compacked_key_cache")
74
+ v.pop("compacked_value_cache")
75
+ if "prefill_stage2_kvcache_size" in v:
76
+ v.pop("prefill_stage2_kvcache_size")
77
+ if "prefill_stage1_kvcache_size" in v:
78
+ v.pop("prefill_stage1_kvcache_size")
79
+ if "recall_topk" in v:
80
+ v.pop("recall_topk")
81
+ return self
82
+
83
+ def get_seq_length(self, layer_idx=0) -> int:
84
+ """
85
+ 返回缓存状态的序列长度
86
+
87
+ Args:
88
+ layer_idx: 可选的层索引
89
+
90
+ Returns:
91
+ int: 序列长度
92
+ """
93
+ is_empty_layer = (
94
+ len(self.key_cache) == 0 # no cache in any layer
95
+ or len(self.key_cache) <= layer_idx # skipped `layer_idx` and hasn't run a layer with cache after it
96
+ or not self.key_cache[layer_idx].numel() # the layer has no cache
97
+ )
98
+ layer_seq_length = self.key_cache[layer_idx].shape[-2] if not is_empty_layer else 0
99
+ return layer_seq_length
100
+
101
+ def copy(self):
102
+ """
103
+ 创建缓存的深拷贝
104
+
105
+ Returns:
106
+ CustomDynamicCache: 缓存的新副本
107
+ """
108
+ new_cache = CustomDynamicCache()
109
+ new_cache.key_cache = [k.clone() for k in self.key_cache]
110
+ new_cache.value_cache = [v.clone() for v in self.value_cache]
111
+ new_cache.cache_kwargs = copy.deepcopy(self.cache_kwargs)
112
+ new_cache.group_cache = copy.deepcopy(self.group_cache)
113
+ new_cache.meta = copy.deepcopy(self.meta)
114
+ new_cache._seen_tokens = self._seen_tokens
115
+ return new_cache
116
+
117
+ def update_router_kcache(
118
+ self,
119
+ key_states: torch.Tensor,
120
+ layer_idx: int,
121
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
122
+
123
+ # Update the cache
124
+ if key_states is not None:
125
+ if len(self.router_key_cache) <= layer_idx:
126
+ # There may be skipped layers, fill them with empty lists
127
+ for _ in range(len(self.router_key_cache), layer_idx):
128
+ self.router_key_cache.append(torch.tensor([]))
129
+ self.router_key_cache.append(key_states)
130
+ elif (
131
+ not self.router_key_cache[layer_idx].numel() # prefers not t.numel() to len(t) == 0 to export the model
132
+ ): # fills previously skipped layers; checking for tensor causes errors
133
+ self.router_key_cache[layer_idx] = key_states
134
+ else:
135
+ self.router_key_cache[layer_idx] = torch.cat([self.router_key_cache[layer_idx], key_states], dim=-2)
136
+
137
+ return self.router_key_cache[layer_idx]
138
+
139
+ class CustomDynamicCacheOnCPU(CustomDynamicCache):
140
+ def __init__(self, _distributed_cache_data=None):
141
+ super().__init__(_distributed_cache_data)
142
+
143
+ def record_kwargs(self, layer_idx, kwargs):
144
+ d = {}
145
+ for k, v in kwargs.items():
146
+ if v is not None and torch.is_tensor(v):
147
+ d[k] = v.cpu() if v.is_cuda else v.clone()
148
+ else:
149
+ d[k] = v
150
+ super().record_kwargs(layer_idx, d)
151
+
152
+ def update(
153
+ self,
154
+ key_states: torch.Tensor,
155
+ value_states: torch.Tensor,
156
+ layer_idx: int,
157
+ cache_kwargs=None,
158
+ ) -> tuple[torch.Tensor, torch.Tensor]:
159
+ if key_states is not None and torch.is_tensor(key_states) and key_states.is_cuda:
160
+ key_states = key_states.cpu()
161
+ if value_states is not None and torch.is_tensor(value_states) and value_states.is_cuda:
162
+ value_states = value_states.cpu()
163
+ return super().update(key_states, value_states, layer_idx, cache_kwargs)
164
+
165
+ def update_router_kcache(
166
+ self,
167
+ key_states: torch.Tensor,
168
+ layer_idx: int,
169
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
170
+ if key_states is not None and torch.is_tensor(key_states) and key_states.is_cuda:
171
+ key_states = key_states.cpu()
172
+ return super().update_router_kcache(key_states, layer_idx)
173
+
174
+ class CustomQuantizeDynamicCache(QuantoQuantizedCache):
175
+ """
176
+ 自定义量化动态缓存类
177
+ 扩展标准的QuantoQuantizedCache,添加额外的元数据存储和查询功能
178
+ """
179
+ def __init__(self, cache_config):
180
+ super().__init__(cache_config)
181
+ self.cache_config = cache_config
182
+ self.cache_kwargs = {}
183
+ self.group_cache = {}
184
+ self.meta = {}
185
+
186
+ def record_kwargs(self, layer_idx, kwargs):
187
+ """
188
+ 记录层的元数据信息
189
+
190
+ Args:
191
+ layer_idx: 层索引
192
+ kwargs: 包含路由层信息的字典
193
+ """
194
+ if layer_idx in self.cache_kwargs:
195
+ self.cache_kwargs[layer_idx].update(kwargs)
196
+ else:
197
+ self.cache_kwargs[layer_idx] = kwargs
198
+
199
+ def get_layer_length(self):
200
+ return len(self.cache_kwargs)
201
+
202
+ def clear_kvcache(self):
203
+ self._quantized_key_cache = []
204
+ self._quantized_value_cache = []
205
+ self.key_cache = []
206
+ self.value_cache = []
207
+
208
+ def get_kvcache(self, layer_idx):
209
+ """
210
+ 获取指定层的KV缓存(反量化后)
211
+
212
+ Args:
213
+ layer_idx: 层索引
214
+
215
+ Returns:
216
+ Tuple[torch.Tensor, torch.Tensor]: (key_cache, value_cache)
217
+ """
218
+ dequant_key = self._dequantize(self._quantized_key_cache[layer_idx])
219
+ dequant_value = self._dequantize(self._quantized_value_cache[layer_idx])
220
+ return dequant_key, dequant_value
221
+
222
+ def update(
223
+ self,
224
+ key_states: torch.Tensor,
225
+ value_states: torch.Tensor,
226
+ layer_idx: int,
227
+ cache_kwargs=None,
228
+ ) -> tuple[torch.Tensor, torch.Tensor]:
229
+ """
230
+ 更新缓存
231
+
232
+ Args:
233
+ key_states: 新的key状态
234
+ value_states: 新的value状态
235
+ layer_idx: 层索引
236
+ cache_kwargs: 缓存关键字参数
237
+
238
+ Returns:
239
+ Tuple[torch.Tensor, torch.Tensor]: 更新后的key和value状态
240
+ """
241
+ # Update the number of seen tokens
242
+ if layer_idx == 0:
243
+ self._seen_tokens += key_states.shape[-2]
244
+
245
+ if len(self.key_cache) < layer_idx:
246
+ for i in range(len(self.key_cache), layer_idx):
247
+ self.key_cache.append(torch.zeros(0, dtype=key_states.dtype, device=key_states.device))
248
+ self.value_cache.append(torch.zeros(0, dtype=key_states.dtype, device=key_states.device))
249
+ self._quantized_key_cache.append(torch.zeros(0, dtype=key_states.dtype, device=key_states.device))
250
+ self._quantized_value_cache.append(torch.zeros(0, dtype=key_states.dtype, device=key_states.device))
251
+
252
+ if len(self.key_cache) == layer_idx:
253
+ self._quantized_key_cache.append(self._quantize(key_states.contiguous(), axis=self.axis_key))
254
+ self._quantized_value_cache.append(self._quantize(value_states.contiguous(), axis=self.axis_value))
255
+ self.key_cache.append(torch.zeros(0, dtype=key_states.dtype, device=key_states.device))
256
+ self.value_cache.append(torch.zeros(0, dtype=key_states.dtype, device=key_states.device))
257
+ keys_to_return, values_to_return = key_states, value_states
258
+ else:
259
+ dequant_key = self._dequantize(self._quantized_key_cache[layer_idx])
260
+ dequant_value = self._dequantize(self._quantized_value_cache[layer_idx])
261
+ keys_to_return = [dequant_key, self.key_cache[layer_idx], key_states]
262
+ values_to_return = [dequant_value, self.value_cache[layer_idx], value_states]
263
+
264
+ keys_to_return = torch.cat(keys_to_return, dim=-2)
265
+ values_to_return = torch.cat(values_to_return, dim=-2)
266
+ if (
267
+ self.key_cache[layer_idx].dim() == 4
268
+ and self.key_cache[layer_idx].shape[-2] + 1 >= self.residual_length
269
+ ):
270
+ self._quantized_key_cache[layer_idx] = self._quantize(keys_to_return.contiguous(), axis=self.axis_key)
271
+ self._quantized_value_cache[layer_idx] = self._quantize(
272
+ values_to_return.contiguous(), axis=self.axis_value
273
+ )
274
+ self.key_cache[layer_idx] = torch.zeros(0, dtype=key_states.dtype, device=key_states.device)
275
+ self.value_cache[layer_idx] = torch.zeros(0, dtype=key_states.dtype, device=key_states.device)
276
+ else:
277
+ self.key_cache[layer_idx] = torch.cat([self.key_cache[layer_idx], key_states], dim=-2)
278
+ self.value_cache[layer_idx] = torch.cat([self.value_cache[layer_idx], value_states], dim=-2)
279
+
280
+ return keys_to_return, values_to_return
281
+
282
+ def get_seq_length(self, layer_idx=0) -> int:
283
+ """
284
+ 返回缓存状态的序列长度
285
+
286
+ Args:
287
+ layer_idx: 可选的层索引
288
+
289
+ Returns:
290
+ int: 序列长度
291
+ """
292
+ is_empty_layer = (
293
+ len(self._quantized_key_cache) == 0 # no cache in any layer
294
+ or len(self._quantized_key_cache) <= layer_idx # skipped `layer_idx` and hasn't run a layer with cache after it
295
+ or not self._quantized_key_cache[layer_idx].numel() # the layer has no cache
296
+ )
297
+ layer_seq_length = self._quantized_key_cache[layer_idx].shape[-2] if not is_empty_layer else 0
298
+ return layer_seq_length
299
+
300
+ def clear_query(self):
301
+ """
302
+ 清理查询相关的临时数据
303
+ 移除查询过程中产生的临时数据,保持缓存清洁
304
+ """
305
+ for k, v in self.cache_kwargs.items():
306
+ if "compacked_key_cache" in v:
307
+ v.pop("compacked_key_cache")
308
+ v.pop("compacked_value_cache")
309
+ if "prefill_stage2_kvcache_size" in v:
310
+ v.pop("prefill_stage2_kvcache_size")
311
+ if "prefill_stage1_kvcache_size" in v:
312
+ v.pop("prefill_stage1_kvcache_size")
313
+ if "recall_topk" in v:
314
+ v.pop("recall_topk")
315
+ return self
316
+
317
+ def copy(self):
318
+ """
319
+ 创建缓存的深拷贝
320
+
321
+ Returns:
322
+ CustomQuantizeDynamicCache: 缓存的新副本
323
+ """
324
+ new_cache = CustomQuantizeDynamicCache(self.cache_config)
325
+ if hasattr(self, '_quantized_key_cache'):
326
+ new_cache._quantized_key_cache = [k.clone() for k in self._quantized_key_cache]
327
+ new_cache._quantized_value_cache = [v.clone() for v in self._quantized_value_cache]
328
+ new_cache.key_cache = [k.clone() for k in self.key_cache]
329
+ new_cache.value_cache = [v.clone() for v in self.value_cache]
330
+ new_cache.cache_kwargs = copy.deepcopy(self.cache_kwargs)
331
+ new_cache.group_cache = copy.deepcopy(self.group_cache)
332
+ new_cache.meta = copy.deepcopy(self.meta)
333
+ new_cache._seen_tokens = self._seen_tokens
334
+ return new_cache
335
+
336
+
337
+ def create_cache(quantize_nbits: Optional[int] = 0):
338
+ """
339
+ 根据参数创建合适的缓存实例
340
+
341
+ Args:
342
+ args: 包含量化相关参数的命名空间对象
343
+
344
+ Returns:
345
+ CustomDynamicCache or CustomQuantizeDynamicCache: 缓存实例
346
+ """
347
+ if quantize_nbits > 0:
348
+ quan_cache_config = QuantizedCacheConfig(nbits=quantize_nbits)
349
+ return CustomQuantizeDynamicCache(quan_cache_config)
350
+ else:
351
+ return CustomDynamicCache()
352
+
353
+ def manual_deepcopy_kv_cache(cache_obj):
354
+ """
355
+ Manually performs a deep copy of a custom KV cache object,
356
+ avoiding the issues with quanto's __deepcopy__.
357
+ """
358
+ # 1. 创建一个新的、空的 cache 对象实例
359
+ if isinstance(cache_obj, CustomQuantizeDynamicCache):
360
+ # 如果是量化缓存,需要传入配置
361
+ new_cache = CustomQuantizeDynamicCache(cache_obj.cache_config)
362
+ elif isinstance(cache_obj, CustomDynamicCache):
363
+ new_cache = CustomDynamicCache()
364
+ else:
365
+ # 如果有其他类型的缓存,可以在这里扩展
366
+ raise TypeError(f"Unsupported cache type for manual deepcopy: {type(cache_obj)}")
367
+
368
+ # 2. 复制非张量元数据
369
+ # 使用标准 deepcopy 是安全的,因为这些是字典和列表
370
+ new_cache.cache_kwargs = copy.deepcopy(cache_obj.cache_kwargs)
371
+ new_cache.meta = copy.deepcopy(cache_obj.meta)
372
+
373
+ # 3. 逐层复制核心的 key-value 张量缓存
374
+ if hasattr(cache_obj, '_quantized_key_cache'): # 处理 QuantoQuantizedCache
375
+ for layer_cache in cache_obj._quantized_key_cache:
376
+ # .clone().detach() 是安全复制张量的标准方法
377
+ new_cache._quantized_key_cache.append(layer_cache.clone().detach())
378
+ for layer_cache in cache_obj._quantized_value_cache:
379
+ new_cache._quantized_value_cache.append(layer_cache.clone().detach())
380
+ new_cache._seen_tokens = cache_obj._seen_tokens
381
+
382
+ if hasattr(cache_obj, 'key_cache'): # 处理 DynamicCache
383
+ for layer_cache in cache_obj.key_cache:
384
+ new_cache.key_cache.append(layer_cache.clone().detach())
385
+ for layer_cache in cache_obj.value_cache:
386
+ new_cache.value_cache.append(layer_cache.clone().detach())
387
+ new_cache._seen_tokens = cache_obj._seen_tokens
388
+
389
+ return new_cache
390
+
391
+ def convert_tensor(data, cuda_device):
392
+ """转换结构体的 tensor device,如果cuda_device非 None,则将cpu 转换到 cuda,否则将 cuda 转换到 cpu
393
+ 支持dict, list, tuple, set
394
+ """
395
+
396
+ converted_count = [0] # 使用列表以便在嵌套函数中修改
397
+
398
+ def _convert_recursive(obj):
399
+ # 如果是torch tensor且在CUDA上
400
+ if torch.is_tensor(obj):
401
+ if cuda_device:
402
+ return obj.to(cuda_device) if obj.is_cpu else obj
403
+ if obj.is_cuda:
404
+ return obj.cpu()
405
+ return obj
406
+
407
+ # 处理各种容器类型
408
+ elif isinstance(obj, dict):
409
+ return {k: _convert_recursive(v) for k, v in obj.items()}
410
+
411
+ elif isinstance(obj, list):
412
+ return [_convert_recursive(item) for item in obj]
413
+
414
+ elif isinstance(obj, tuple):
415
+ # 元组不可变,总是创建新的
416
+ return tuple(_convert_recursive(item) for item in obj)
417
+
418
+ elif isinstance(obj, set):
419
+ return {_convert_recursive(item) for item in obj}
420
+
421
+ # 其他数据类型直接返回
422
+ else:
423
+ return obj
424
+
425
+ return _convert_recursive(data)
426
+
427
+ def copy_dict_to_cpu(d: dict):
428
+ ret = {}
429
+ for k, v in d.items():
430
+ ret[k] = v.cpu() if torch.is_tensor(v) and v.is_cuda else v
431
+ return ret
432
+
433
+ def copy_dict_to_gpu(d: dict, device):
434
+ if not d:
435
+ return d
436
+ ret = {}
437
+ for k, v in d.items():
438
+ ret[k] = v.to(device) if torch.is_tensor(v) and not v.is_cuda else v
439
+ return ret
440
+
441
+ def copy_kv_cache_to_device(cache_obj, cuda_device, copy_v: bool=True):
442
+ if isinstance(cache_obj, CustomQuantizeDynamicCache):
443
+ new_cache = CustomQuantizeDynamicCache(cache_obj.cache_config)
444
+ elif isinstance(cache_obj, CustomDynamicCache):
445
+ new_cache = CustomDynamicCache()
446
+ else:
447
+ raise TypeError(f"Unsupported cache type for manual deepcopy: {type(cache_obj)}")
448
+
449
+ # 复制非张量元数据
450
+ new_cache.cache_kwargs = convert_tensor(cache_obj.cache_kwargs, cuda_device)
451
+ new_cache.meta = convert_tensor(cache_obj.meta, cuda_device)
452
+
453
+ # 复制核心张量缓存
454
+ if hasattr(cache_obj, '_quantized_key_cache'):
455
+ new_cache._quantized_key_cache = convert_tensor(cache_obj._quantized_key_cache, cuda_device)
456
+ if copy_v:
457
+ new_cache._quantized_value_cache = convert_tensor(cache_obj._quantized_value_cache, cuda_device)
458
+ else:
459
+ new_cache._quantized_value_cache = cache_obj._quantized_value_cache
460
+ new_cache._seen_tokens = cache_obj._seen_tokens
461
+
462
+ if hasattr(cache_obj, 'key_cache'):
463
+ new_cache.key_cache = convert_tensor(cache_obj.key_cache, cuda_device)
464
+ if copy_v:
465
+ new_cache.value_cache = convert_tensor(cache_obj.value_cache, cuda_device)
466
+ else:
467
+ new_cache.value_cache = cache_obj.value_cache
468
+ new_cache._seen_tokens = cache_obj._seen_tokens
469
+
470
+ return new_cache
471
+
472
+ # def copy_kv_cache_to_gpu(cache_obj, device, copy_v: bool=True):
473
+ # """
474
+ # 手动执行缓存对象的深拷贝
475
+ # 避免quanto库__deepcopy__的问题
476
+
477
+ # Args:
478
+ # cache_obj: 要拷贝的缓存对象
479
+
480
+ # Returns:
481
+ # 缓存对象的深拷贝副本
482
+ # """
483
+ # if isinstance(cache_obj, CustomQuantizeDynamicCache):
484
+ # new_cache = CustomQuantizeDynamicCache(cache_obj.cache_config)
485
+ # elif isinstance(cache_obj, CustomDynamicCache):
486
+ # new_cache = CustomDynamicCache()
487
+ # else:
488
+ # raise TypeError(f"Unsupported cache type for manual deepcopy: {type(cache_obj)}")
489
+
490
+ # # 复制非张量元数据
491
+ # new_cache.cache_kwargs = copy_dict_to_gpu(cache_obj.cache_kwargs, device)
492
+ # new_cache.meta = copy_dict_to_gpu(cache_obj.meta, device)
493
+
494
+ # # 复制核心张量缓存
495
+ # if hasattr(cache_obj, '_quantized_key_cache'):
496
+ # new_cache._quantized_key_cache = [t.to(device) for t in cache_obj._quantized_key_cache]
497
+ # if copy_v:
498
+ # new_cache._quantized_value_cache = [t.to(device) for t in cache_obj._quantized_value_cache]
499
+ # else:
500
+ # new_cache._quantized_value_cache = cache_obj._quantized_value_cache
501
+ # new_cache._seen_tokens = cache_obj._seen_tokens
502
+
503
+ # if hasattr(cache_obj, 'key_cache'):
504
+ # new_cache.key_cache = [t.to(device) for t in cache_obj.key_cache]
505
+ # if copy_v:
506
+ # new_cache.value_cache = [t.to(device) for t in cache_obj.value_cache]
507
+ # else:
508
+ # new_cache.value_cache = cache_obj.value_cache
509
+
510
+ # new_cache._seen_tokens = cache_obj._seen_tokens
511
+
512
+ # return new_cache
src/utils/callbacks.py ADDED
@@ -0,0 +1,172 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import typing
4
+ # from deepspeed.accelerator import get_accelerator
5
+
6
+ # NOTE: 最新版开始迁移到 integration_utils
7
+ try:
8
+ from transformers.integrations import TrainerCallback
9
+ except ImportError:
10
+ from transformers.integrations.integration_utils import TrainerCallback
11
+
12
+ # USE_FLASH_ATTN, USE_XFORMERS_ATTN = False, False
13
+ # if os.getenv('FLASH_ATTN', 'false').lower() == 'true':
14
+ # USE_FLASH_ATTN = True
15
+ # from mllm.utils.llama_flash_attn_monkey_patch import replace_llama_attn_with_flash_attn, restore_llama_attn
16
+ # if os.getenv("XFORMERS_ATTN", 'false').lower() == 'true':
17
+ # USE_XFORMERS_ATTN = True
18
+ # from mllm.utils.llama_xformers_monkey_patch import replace_llama_attn_with_xformers_attn, restore_llama_attn
19
+
20
+ class ModeltimeCallback(TrainerCallback):
21
+ def __init__(self):
22
+ self.model_time = 0.
23
+ self.data_time = 0.
24
+ self._start = 0.
25
+ self._end = 0.
26
+
27
+ def on_train_begin(self, args, state, control, **kwargs):
28
+ self._end = time.time()
29
+
30
+ def on_step_begin(self, args, state, control, **kwargs):
31
+ self._start = time.time()
32
+ self.data_time += (self._start - self._end)
33
+
34
+ def on_step_end(self, args, state, control, **kwargs):
35
+ self._end = time.time()
36
+ self.model_time += (self._end - self._start)
37
+
38
+ def on_log(self, args, state, control, logs=None, **kwargs):
39
+ if not state.is_world_process_zero:
40
+ return
41
+
42
+ data_time = self.data_time / state.logging_steps
43
+ model_time = self.model_time / state.logging_steps
44
+ self.data_time = 0.
45
+ self.model_time = 0.
46
+
47
+ info = f'\nSTEP: {state.global_step}, data_time: {data_time:.3f}, model_time: {model_time:.3f}'
48
+ print(info)
49
+
50
+
51
+ class SacredCallback(TrainerCallback):
52
+ def __init__(self, _run=None):
53
+ self._run = _run
54
+ self.model_time = 0.
55
+ self.data_time = 0.
56
+ self._start = 0.
57
+ self._end = 0.
58
+ self._zero_loss_cnt = 0
59
+
60
+ def on_train_begin(self, args, state, control, **kwargs):
61
+ if self._run:
62
+ self._end = time.time()
63
+
64
+ def on_step_begin(self, args, state, control, **kwargs):
65
+ if self._run:
66
+ self._start = time.time()
67
+ self.data_time += (self._start - self._end)
68
+
69
+ def on_step_end(self, args, state, control, **kwargs):
70
+ if self._run:
71
+ self._end = time.time()
72
+ self.model_time += (self._end - self._start)
73
+
74
+ def on_log(self, args, state, control, logs=None, **kwargs):
75
+ if not state.is_world_process_zero:
76
+ return
77
+
78
+ if self._run is None:
79
+ return
80
+
81
+ data_time = self.data_time / state.logging_steps
82
+ model_time = self.model_time / state.logging_steps
83
+ self.data_time = 0.
84
+ self.model_time = 0.
85
+
86
+ self._run.log_scalar("loss", logs.get("loss", 0.), state.global_step)
87
+ self._run.log_scalar("learning_rate", logs.get("learning_rate", 0.), state.global_step)
88
+ self._run.log_scalar("grad_norm", logs.get("grad_norm", 0.), state.global_step)
89
+ self._run.log_scalar("epoch", state.epoch, state.global_step)
90
+ self._run.log_scalar("data_time", data_time, state.global_step)
91
+ self._run.log_scalar("model_time", model_time, state.global_step)
92
+
93
+ if logs.get("loss", 0.) == 0.:
94
+ self._zero_loss_cnt += 1
95
+ if self._zero_loss_cnt > 1:
96
+ raise RuntimeError("Loss is zero, something is wrong!")
97
+
98
+
99
+ class ModelEvalCallback(TrainerCallback):
100
+ def __init__(self, _run=None, multitest=None, trainer=None, gen_kwargs=None):
101
+ self._run = _run
102
+ # datasets dict
103
+ # key1: dataset_name
104
+ # val1: {'dataset': dataset inst, 'compute_metric': metric inst}
105
+ self.multitest = typing.cast(dict, multitest)
106
+ self.trainer = trainer
107
+ self.gen_kwargs = gen_kwargs
108
+
109
+ def on_step_end(self, args, state, control, **kwargs):
110
+ if args.eval_steps is None:
111
+ eval_steps = args.save_steps
112
+ elif isinstance(args.eval_steps, int) and args.eval_steps > 0:
113
+ eval_steps = args.eval_steps
114
+ else:
115
+ return
116
+ if state.global_step > 0 and state.global_step % eval_steps == 0:
117
+ if not args.do_multi_predict:
118
+ return
119
+
120
+ # flash-attn currently not supports eval mode!
121
+ # if USE_FLASH_ATTN or USE_XFORMERS_ATTN:
122
+ # restore_llama_attn()
123
+
124
+ old_compute_metrics = self.trainer.compute_metrics
125
+
126
+ for dataset_idx, (dataset_name, item) in enumerate(self.multitest.items()):
127
+ print(f'processing multitest set {dataset_idx}/{len(self.multitest)}: {dataset_name}')
128
+ _ds = item['dataset']
129
+ _compute_metrics = item['compute_metric']
130
+ _prefix = dataset_name
131
+
132
+ self.trainer.compute_metrics = _compute_metrics
133
+ # transformers.trainer_utils.PredictionOutput
134
+ _pred_results = self.trainer.predict(_ds, metric_key_prefix=_prefix, **self.gen_kwargs)
135
+ if state.is_world_process_zero:
136
+ self.trainer.log_metrics(_prefix, _pred_results.metrics) # noqa
137
+ self.trainer.save_metrics(_prefix, _pred_results.metrics) # noqa
138
+ self.trainer.save_prediction(_pred_results, file_key_prefix=_prefix)
139
+
140
+ if self._run is not None:
141
+ keywords_to_remove = ['runtime', 'second']
142
+ for k, v in _pred_results.metrics.items():
143
+ # remove time releated metrics
144
+ if any(kw in k for kw in keywords_to_remove):
145
+ continue
146
+ self._run.log_scalar(f'{k}', v, state.global_step)
147
+
148
+ self.trainer.compute_metrics = old_compute_metrics
149
+
150
+ # if USE_FLASH_ATTN:
151
+ # replace_llama_attn_with_flash_attn()
152
+ # if USE_XFORMERS_ATTN:
153
+ # replace_llama_attn_with_xformers_attn()
154
+
155
+ class DSEmptyCacheCallback(TrainerCallback):
156
+ def on_step_end(self, args, state, control, **kwargs):
157
+ empty_cache_steps = int(os.getenv("EMPTY_CACHE_STEP", '0').strip())
158
+ can_flush = state.global_step > 0 and empty_cache_steps > 0 and state.global_step % empty_cache_steps == 0
159
+ if can_flush:
160
+ # print('Flush Cache here.')
161
+ get_accelerator().empty_cache()
162
+
163
+
164
+ # usage: https://github.com/yqhu/profiler-workshop/blob/c8d4a7c30a61cc7b909d89f88f5fd36b70c55769/hf_training_trainer_prof.py#L49C6-L49C28
165
+ # additionally, with_modules can be set True, with_flops must be set False
166
+ # deps: pip install -U tensorboard-plugin-profilepip torch_tb_profiler
167
+ class ProfCallback(TrainerCallback):
168
+ def __init__(self, prof):
169
+ self.prof = prof
170
+
171
+ def on_step_end(self, args, state, control, **kwargs):
172
+ self.prof.step()
src/utils/common.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ from typing import List, Union, Dict
3
+
4
+ import PIL.Image
5
+ import torch
6
+ import numpy as np
7
+ import torchvision.transforms.functional as F
8
+ import transformers
9
+
10
+ from transformers import PreTrainedTokenizer
11
+
12
+ IGNORE_INDEX = -100
13
+
14
+ def print_trainable_params(model: torch.nn.Module) -> None:
15
+ trainable_params, all_param = 0, 0
16
+ for param in model.parameters():
17
+ num_params = param.numel()
18
+ # if using DS Zero 3 and the weights are initialized empty
19
+ if num_params == 0 and hasattr(param, "ds_numel"):
20
+ num_params = param.ds_numel
21
+ all_param += num_params
22
+ if param.requires_grad:
23
+ trainable_params += num_params
24
+ print("trainable params: {:d} || all params: {:d} || trainable%: {:.4f}".format(
25
+ trainable_params, all_param, 100 * trainable_params / all_param))
26
+
27
+
28
+ def post_process_generate_ids(tokenizer: PreTrainedTokenizer, ids: torch.Tensor):
29
+ ids = copy.deepcopy(ids) # do not modify origin preds and targets
30
+ ids[ids < 0] = tokenizer.pad_token_id
31
+ # pad_to_multiof 开启后, 多余的部分没法解码, 这里暂时替换为 ','
32
+ ids[ids >= len(tokenizer)] = tokenizer.convert_tokens_to_ids(',')
33
+ return ids
34
+
35
+
36
+ def decode_generate_ids(tokenizer: PreTrainedTokenizer, ids: torch.Tensor) -> Union[List[str], str]:
37
+ assert ids.ndim in [1, 2]
38
+ only_one_sentence = ids.ndim == 1
39
+ if only_one_sentence:
40
+ ids = ids.unsqueeze(0)
41
+ ids = post_process_generate_ids(tokenizer, ids)
42
+ res = tokenizer.batch_decode(ids, skip_special_tokens=True, clean_up_tokenization_spaces=True)
43
+ if only_one_sentence:
44
+ return res[0]
45
+ return res
46
+
47
+
48
+
49
+ def draw_bounding_boxes(
50
+ image: Union[torch.Tensor, PIL.Image.Image],
51
+ boxes: Union[torch.Tensor, List, np.ndarray],
52
+ **kwargs,
53
+ ):
54
+ if isinstance(image, PIL.Image.Image):
55
+ from torchvision.transforms import PILToTensor
56
+ image = PILToTensor()(image)
57
+ assert isinstance(image, torch.Tensor), ""
58
+
59
+ if not isinstance(boxes, torch.Tensor):
60
+ boxes = torch.as_tensor(boxes)
61
+ assert isinstance(boxes, torch.Tensor)
62
+
63
+ from torchvision.utils import draw_bounding_boxes as _draw_bounding_boxes
64
+ return _draw_bounding_boxes(image, boxes, **kwargs)
65
+
66
+
67
+ # https://github.com/huggingface/tokenizers/issues/247#issuecomment-675458087
68
+ def smart_tokenizer_and_embedding_resize(
69
+ special_tokens_dict: Dict,
70
+ tokenizer: transformers.PreTrainedTokenizer,
71
+ model: transformers.PreTrainedModel,
72
+ ):
73
+ """Resize tokenizer and embedding.
74
+
75
+ Note: This is the unoptimized version that may make your embedding size not be divisible by 64.
76
+ """
77
+ num_new_tokens = tokenizer.add_special_tokens(special_tokens_dict)
78
+ model.resize_token_embeddings(len(tokenizer))
79
+
80
+ if num_new_tokens > 0:
81
+ input_embeddings = model.get_input_embeddings().weight.data
82
+ output_embeddings = model.get_output_embeddings().weight.data
83
+
84
+ input_embeddings_avg = input_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
85
+ output_embeddings_avg = output_embeddings[:-num_new_tokens].mean(dim=0, keepdim=True)
86
+
87
+ input_embeddings[-num_new_tokens:] = input_embeddings_avg
88
+ output_embeddings[-num_new_tokens:] = output_embeddings_avg
89
+
90
+ def patch_transformer_logging():
91
+ import logging
92
+ import transformers
93
+ def enable_explicit_format():
94
+ handlers = transformers.utils.logging._get_library_root_logger().handlers
95
+
96
+ for handler in handlers:
97
+ formatter = logging.Formatter("[(%(levelname)s) %(pathname)s:%(lineno)s ] %(asctime)s >> %(message)s")
98
+ handler.setFormatter(formatter)
99
+ transformers.utils.logging.enable_explicit_format = enable_explicit_format
100
+
101
+ def print_model_stats(model):
102
+ """
103
+ 通用模型参数统计工具。
104
+ 自动识别是 Dense 还是 MoE 模型,并计算 Total vs Active 参数量。
105
+ """
106
+
107
+ # 1. 计算物理总参数量 (Total Parameters)
108
+ # 使用 set 避免计算共享参数 (Shared Weights),例如 Embedding 和 lm_head 共享权重的情况
109
+ unique_params = {p.data_ptr(): p for p in model.parameters()}.values()
110
+ total_params = sum(p.numel() for p in unique_params)
111
+
112
+ # 2. 初始化激活参数量 (Active Parameters)
113
+ # 默认假设是 Dense 模型,所有参数都是激活的
114
+ active_params = total_params
115
+
116
+ moe_infos = [] # 用于存储发现的 MoE 层信息
117
+
118
+ # 3. 遍历所有子模块,寻找 MoE 层特征
119
+ # 我们不匹配类名,而是匹配"特征" (Duck Typing)
120
+ for name, module in model.named_modules():
121
+ # 特征判定:有 num_experts 属性,且有一个叫 experts 的 ModuleList
122
+ if hasattr(module, 'num_experts') and hasattr(module, 'experts') and isinstance(module.experts, nn.ModuleList):
123
+
124
+ # 获取关键超参
125
+ num_experts = getattr(module, 'num_experts', 0)
126
+ # 兼容不同的 top_k 命名 (top_k 或 num_experts_per_tok)
127
+ top_k = getattr(module, 'top_k', getattr(module, 'num_experts_per_tok', 0))
128
+
129
+ # 如果找不到 top_k,可能不是标准的 Sparse MoE,跳过
130
+ if top_k == 0:
131
+ continue
132
+
133
+ # --- 核心计算逻辑 ---
134
+ # 1. 计算单个专家的参数量 (假设所有专家结构相同,取第一个)
135
+ # 这里必须用 recursion=True 确保统计专家内部所有层
136
+ single_expert_params = sum(p.numel() for p in module.experts[0].parameters())
137
+
138
+ # 2. 计算"休眠"专家数量
139
+ dormant_experts = num_experts - top_k
140
+
141
+ # 3. 从激活总数中扣除休眠专家的参数
142
+ # 注意:total_params 里已经包含了 N 个专家,我们只需要减去 (N-K) 个
143
+ if dormant_experts > 0:
144
+ deduction = dormant_experts * single_expert_params
145
+ active_params -= deduction
146
+
147
+ moe_infos.append({
148
+ "layer": name,
149
+ "experts": num_experts,
150
+ "active": top_k,
151
+ "expert_size": single_expert_params
152
+ })
153
+
154
+ # --- 4. 格式化输出 ---
155
+ def format_num(num):
156
+ if num >= 1e9: return f"{num/1e9:.2f}B"
157
+ if num >= 1e6: return f"{num/1e6:.2f}M"
158
+ if num >= 1e3: return f"{num/1e3:.2f}K"
159
+ return str(num)
160
+
161
+ print("=" * 50)
162
+ print(f"Model Architecture Analysis")
163
+ print("=" * 50)
164
+
165
+ if len(moe_infos) > 0:
166
+ print(f"👉 Detection: MoE Model (Sparse Mixture-of-Experts)")
167
+ print(f" - Found {len(moe_infos)} MoE layers")
168
+ print(f" - Config: {moe_infos[0]['experts']} Experts, Top-{moe_infos[0]['active']} Active")
169
+ else:
170
+ print(f"👉 Detection: Dense Model (Standard Transformer)")
171
+
172
+ print("-" * 50)
173
+ print(f"Total Parameters (VRAM): {format_num(total_params)}")
174
+ print(f"Active Parameters (FLOPs): {format_num(active_params)}")
175
+
176
+ if len(moe_infos) > 0:
177
+ sparsity = 1 - (active_params / total_params)
178
+ print(f"Sparsity Ratio: {sparsity:.2%}")
179
+ # 计算相比 Dense 版本的倍数
180
+ # 假设 Dense 版本就是 active_params 大小(不太严谨但直观)
181
+ print(f"Upcycling Scale: {total_params/active_params:.2f}x Larger than Dense Base")
182
+ else:
183
+ print(f"Sparsity Ratio: 0.00% (Dense)")
184
+
185
+ print("=" * 50)
186
+
187
+ return total_params, active_params
src/utils/data_utils.py ADDED
@@ -0,0 +1,318 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import lmdb
3
+ from multiprocessing import Pool
4
+ from tqdm import tqdm
5
+ import importlib.metadata
6
+ import importlib.util
7
+ from packaging import version
8
+ from typing import TYPE_CHECKING
9
+ from functools import lru_cache
10
+ from datasets import Dataset, IterableDataset
11
+ from src.utils.common import IGNORE_INDEX
12
+ from collections import defaultdict
13
+ from functools import partial
14
+ import bisect
15
+ from typing import List, Sequence, Tuple, Optional, Union
16
+ from src.utils.common import pdb_debug
17
+
18
+ if TYPE_CHECKING:
19
+ from packaging.version import Version
20
+
21
+ def write_lmdb(output_dir, name):
22
+
23
+ os.makedirs(output_dir, exist_ok=True)
24
+ output_name = os.path.join(output_dir, f'{name}.lmdb')
25
+
26
+ try:
27
+ os.remove(output_name)
28
+ except:
29
+ pass
30
+ env_new = lmdb.open(
31
+ output_name,
32
+ subdir=False,
33
+ readonly=False,
34
+ lock=False,
35
+ readahead=False,
36
+ meminit=False,
37
+ max_readers=1,
38
+ map_size=int(100e9),
39
+ )
40
+ txn_write = env_new.begin(write=True)
41
+
42
+ return txn_write, env_new
43
+
44
+ def read_lmdb(lmdb_path):
45
+ env = lmdb.open(
46
+ lmdb_path,
47
+ subdir=False,
48
+ readonly=True,
49
+ lock=False,
50
+ readahead=False,
51
+ meminit=False,
52
+ max_readers=256,
53
+ )
54
+ txn = env.begin()
55
+ return env, txn
56
+
57
+
58
+ def get_length(index):
59
+ return index, len(global_dataset[index]["input_ids"])
60
+
61
+ def get_sequence_length(dataset, num_worker=16):
62
+ global global_dataset
63
+ global_dataset = dataset
64
+ num_data = len(dataset)
65
+ lengths = [0] * num_data
66
+ with Pool(processes=num_worker) as pool:
67
+ iters = pool.imap(get_length, range(num_data))
68
+ for i, length in tqdm(iters, total=num_data):
69
+ lengths[i] = length
70
+ return lengths
71
+
72
+ def get_data(index):
73
+ item = global_torch_dataset[index]
74
+ length = len(global_torch_dataset[index]["input_ids"])
75
+ return item, index, length
76
+
77
+ def torch_dataset_to_hf_dataset(torch_dataset, num_worker=16):
78
+ global global_torch_dataset
79
+ global_torch_dataset = torch_dataset
80
+ num_data = len(global_torch_dataset)
81
+ lengths = [0] * num_data
82
+ hf_dict = {key: [] for key in torch_dataset[0].keys()}
83
+ with Pool(processes=num_worker) as pool:
84
+ iters = pool.imap(get_data, range(num_data))
85
+ for data, i, length in tqdm(iters, total=num_data):
86
+ for key, value in data.items():
87
+ hf_dict[key].append(value)
88
+ lengths[i] = length
89
+ hf_dataset = Dataset.from_dict(hf_dict)
90
+ return hf_dataset, lengths
91
+
92
+ def _get_package_version(name: str) -> "Version":
93
+ try:
94
+ return version.parse(importlib.metadata.version(name))
95
+ except Exception:
96
+ return version.parse("0.0.0")
97
+
98
+ @lru_cache
99
+ def is_transformers_version_greater_than(content: str):
100
+ return _get_package_version("transformers") >= version.parse(content)
101
+
102
+ @lru_cache
103
+ def is_transformers_version_equal_to_4_46():
104
+ return version.parse("4.46.0") <= _get_package_version("transformers") <= version.parse("4.46.1")
105
+
106
+ def search_for_fit(numbers: Sequence[int], capacity: int) -> int:
107
+ r"""
108
+ Finds the index of largest number that fits into the knapsack with the given capacity.
109
+ """
110
+ index = bisect.bisect(numbers, capacity)
111
+ return -1 if index == 0 else (index - 1)
112
+
113
+ def greedy_knapsack(numbers: List[int], capacity: int) -> List[List[int]]:
114
+ r"""
115
+ An efficient greedy algorithm with binary search for the knapsack problem.
116
+ """
117
+ numbers.sort() # sort numbers in ascending order for binary search
118
+ knapsacks = []
119
+
120
+ while numbers:
121
+ current_knapsack = []
122
+ remaining_capacity = capacity
123
+
124
+ while True:
125
+ index = search_for_fit(numbers, remaining_capacity)
126
+ if index == -1:
127
+ break # no more numbers fit in this knapsack
128
+
129
+ remaining_capacity -= numbers[index] # update the remaining capacity
130
+ current_knapsack.append(numbers.pop(index)) # add the number to knapsack
131
+
132
+ knapsacks.append(current_knapsack)
133
+
134
+ return knapsacks
135
+
136
+ def preprocess_packed_supervised_dataset(examples, tokenizer, cutoff_len):
137
+ valid_num = 0
138
+ batch_input_ids, batch_labels = [], []
139
+ lengths = []
140
+ length2indexes = defaultdict(list)
141
+ for i in range(len(examples["input_ids"])):
142
+ input_ids, labels = examples["input_ids"][i], examples["labels"][i]
143
+ length = len(input_ids)
144
+ if length >= cutoff_len - 1:
145
+ continue
146
+ else:
147
+ lengths.append(length)
148
+ length2indexes[length].append(valid_num)
149
+ batch_input_ids.append(input_ids)
150
+ batch_labels.append(labels)
151
+ valid_num += 1
152
+ model_inputs = defaultdict(list)
153
+ knapsacks = greedy_knapsack(lengths, cutoff_len - 1)
154
+ for knapsack in knapsacks:
155
+ packed_input_ids, packed_attention_masks, packed_labels = [], [], []
156
+ for i, length in enumerate(knapsack):
157
+ index = length2indexes[length].pop()
158
+ packed_input_ids += batch_input_ids[index]
159
+ packed_labels += batch_labels[index]
160
+ packed_attention_masks += [1] * len(batch_input_ids[index])
161
+
162
+ if len(packed_input_ids) < cutoff_len:
163
+ pad_length = cutoff_len - len(packed_input_ids)
164
+ packed_input_ids += [tokenizer.pad_token_id] * pad_length
165
+ packed_labels += [IGNORE_INDEX] * pad_length
166
+ packed_attention_masks += [1] * pad_length # more efficient flash_attn
167
+
168
+ if len(packed_input_ids) != cutoff_len:
169
+ raise ValueError("The length of packed example should be identical to the cutoff length.")
170
+
171
+ model_inputs["input_ids"].append(packed_input_ids)
172
+ model_inputs["attention_mask"].append(packed_attention_masks)
173
+ model_inputs["position_ids"].append(list(range(len(packed_input_ids))))
174
+ model_inputs["labels"].append(packed_labels)
175
+ return model_inputs
176
+
177
+ def pad_sequence(examples, cutoff_len, tokenizer):
178
+ max_length = cutoff_len
179
+ input_pad_token_id = tokenizer.pad_token_id
180
+ label_pad_token_id = IGNORE_INDEX
181
+
182
+ for k, v in examples.items():
183
+ if k.endswith("input_ids"):
184
+ pad_token_id = input_pad_token_id
185
+ elif k.endswith("labels"):
186
+ pad_token_id = label_pad_token_id
187
+ # shift labels here
188
+ for i in range(len(v)):
189
+ v[i] = v[i][1:]
190
+ elif k.endswith("attention_mask"):
191
+ pad_token_id = 0
192
+ elif k.endswith("position_ids"):
193
+ pad_token_id = max_length - 1 # pad the max position id
194
+ elif k == "images" or k == "videos":
195
+ pad_token_id = -1
196
+ continue # TODO: haven't tested multi-modal yet
197
+ else:
198
+ continue
199
+ for i in range(len(v)):
200
+ v[i].extend([pad_token_id] * (max_length - len(v[i])))
201
+ examples[k] = v
202
+
203
+ return examples
204
+
205
+ def preprocess_sp_dataset(seq_ids, world_size, sequence_parallel_mode):
206
+ if sequence_parallel_mode == "zigzag-ring":
207
+ step = len(seq_ids) // (2 * world_size)
208
+ value_chunks = [seq_ids[s : s + step] for s in range(0, len(seq_ids), step)]
209
+ local_values = list()
210
+ for rank in range(world_size):
211
+ local_values.append(value_chunks[rank] + value_chunks[2 * world_size - rank - 1])
212
+ return local_values
213
+ elif sequence_parallel_mode == "ulysses":
214
+ step = len(seq_ids) // world_size
215
+ local_values = [seq_ids[s : s + step] for s in range(0, len(seq_ids), step)]
216
+ return local_values
217
+ else:
218
+ raise NotImplementedError("Other sequence parallel modes are to be implemented.")
219
+
220
+
221
+ # sp for Sequence Parallel
222
+ def sp_split(examples, sequence_parallel_size, sequence_parallel_mode="ulysses"):
223
+ for k, v in examples.items():
224
+ chunks = list()
225
+ for row in v:
226
+ if k.endswith("attention_mask"):
227
+ chunks.extend([row] * sequence_parallel_size)
228
+ elif row is None:
229
+ chunks.extend([None] * sequence_parallel_size)
230
+ else:
231
+ chunks.extend(
232
+ preprocess_sp_dataset(row, sequence_parallel_size, sequence_parallel_mode)
233
+ )
234
+ examples[k] = chunks
235
+ return examples
236
+
237
+ def get_sequence_parallel_preprocess(stage, tokenizer, cutoff_len=None, sequence_parallel_size=1, sequence_parallel_mode="ulysses"):
238
+ if stage == "pad":
239
+ assert cutoff_len is not None
240
+ preprocess_func = partial(pad_sequence, cutoff_len=cutoff_len, tokenizer=tokenizer)
241
+ elif stage == "split":
242
+ preprocess_func = partial(sp_split, sequence_parallel_size=sequence_parallel_size, sequence_parallel_mode=sequence_parallel_mode)
243
+ else:
244
+ raise NotImplementedError(f"Unexpected stage in sequence_parallel_preprocess: {stage}")
245
+
246
+ return preprocess_func
247
+
248
+ def _get_sequence_parallel_dataset(dataset, num_works, tokenizer=None, cutoff_len=10000,
249
+ sequence_parallel_size=1, sequence_parallel_mode="ulysses",
250
+ cache_dataset_overwrite=False) -> Optional[Union["Dataset", "IterableDataset"]]:
251
+ kwargs = dict(
252
+ num_proc=num_works,
253
+ load_from_cache_file=not cache_dataset_overwrite,
254
+ desc="Running padding split on dataset",
255
+ )
256
+ pad_sequence_func = get_sequence_parallel_preprocess(
257
+ stage="pad",
258
+ tokenizer=tokenizer,
259
+ cutoff_len=cutoff_len
260
+ )
261
+ padded_dataset = dataset.map(
262
+ pad_sequence_func, batched=True, batch_size=num_works, **kwargs
263
+ )
264
+ kwargs = dict(
265
+ num_proc=num_works,
266
+ load_from_cache_file=not cache_dataset_overwrite,
267
+ desc="Running sequence parallel split on dataset",
268
+ )
269
+ sp_dataset_func = get_sequence_parallel_preprocess(
270
+ stage="split",
271
+ tokenizer=tokenizer,
272
+ sequence_parallel_size=sequence_parallel_size,
273
+ sequence_parallel_mode=sequence_parallel_mode,
274
+ )
275
+ sp_dataset = padded_dataset.map(
276
+ sp_dataset_func, batched=True, batch_size=num_works, **kwargs
277
+ )
278
+ return sp_dataset
279
+
280
+ def packing_dataset(dataset, tokenizer, cutoff_len, num_worker, cache_dataset_overwrite):
281
+ preprocess_func = partial(preprocess_packed_supervised_dataset, tokenizer=tokenizer, cutoff_len=cutoff_len)
282
+ kwargs = dict(
283
+ num_proc=num_worker,
284
+ load_from_cache_file=not cache_dataset_overwrite,
285
+ desc="Running postprocess on dataset",
286
+ )
287
+ import pdb; pdb.set_trace()
288
+ dataset = dataset.map(
289
+ preprocess_func,
290
+ batched=True,
291
+ batch_size=num_worker,
292
+ **kwargs,
293
+ )
294
+ return dataset
295
+
296
+ def data_post_process_sequence_parallel(
297
+ dataset,
298
+ training_args,
299
+ sequence_parallel_size,
300
+ sequence_parallel_mode,
301
+ cutoff_len,
302
+ num_worker=16,
303
+ packing=False,
304
+ tokenizer=None,
305
+ cache_dataset_overwrite=False,
306
+ ):
307
+ dataset = dataset.shuffle(seed=training_args.seed)
308
+ if packing:
309
+ dataset = packing_dataset(dataset, tokenizer, cutoff_len, num_worker, cache_dataset_overwrite)
310
+
311
+ dataset = _get_sequence_parallel_dataset(dataset,
312
+ num_works=num_worker,
313
+ tokenizer=tokenizer,
314
+ cutoff_len=cutoff_len,
315
+ sequence_parallel_size=sequence_parallel_size,
316
+ sequence_parallel_mode=sequence_parallel_mode,
317
+ cache_dataset_overwrite=cache_dataset_overwrite)
318
+ return dataset
src/utils/gpu_monitor.py ADDED
@@ -0,0 +1,384 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pynvml
2
+ import time
3
+ import threading
4
+ from datetime import datetime
5
+ import sys
6
+ import os
7
+
8
+ class GPUMemoryMonitor:
9
+ def __init__(self, gpu_index=0, interval=1.0, unit='GB'):
10
+ """
11
+ 初始化GPU显存监控器
12
+
13
+ Args:
14
+ gpu_index: 要监控的GPU索引,默认0
15
+ interval: 监控间隔时间(秒),默认1秒
16
+ unit: 返回的单位,支持 'MB' 或 'GB',默认'GB'
17
+ """
18
+ self.gpu_index = gpu_index
19
+ self.interval = interval
20
+ self.unit = unit.upper()
21
+
22
+ # 验证单位参数
23
+ if self.unit not in ['MB', 'GB']:
24
+ raise ValueError("单位必须是 'MB' 或 'GB'")
25
+
26
+ # 监控相关状态
27
+ self.monitor_thread = None
28
+ self._running = False
29
+ self._lock = threading.Lock()
30
+ self.peak_memory_usage = 0 # 峰值显存使用量
31
+ self.start_time = None
32
+ self.stop_time = None
33
+
34
+ # 初始化NVML
35
+ try:
36
+ pynvml.nvmlInit()
37
+ self.device_count = pynvml.nvmlDeviceGetCount()
38
+ if self.gpu_index >= self.device_count:
39
+ raise ValueError(f"GPU索引 {self.gpu_index} 超出范围,系统只有 {self.device_count} 个GPU")
40
+ self.handle = pynvml.nvmlDeviceGetHandleByIndex(self.gpu_index)
41
+
42
+ # 获取GPU名称
43
+ self.gpu_name = pynvml.nvmlDeviceGetName(self.handle)
44
+
45
+ except Exception as e:
46
+ print(f"初始化NVML失败: {e}")
47
+ if 'pynvml' in sys.modules:
48
+ pynvml.nvmlShutdown()
49
+ raise
50
+
51
+ def _get_memory_info(self):
52
+ """获取当前GPU显存信息"""
53
+ try:
54
+ mem_info = pynvml.nvmlDeviceGetMemoryInfo(self.handle)
55
+ return mem_info
56
+ except Exception as e:
57
+ print(f"获取GPU {self.gpu_index} 显存信息失败: {e}")
58
+ return None
59
+
60
+ def _convert_units(self, bytes_value):
61
+ """转换字节为指定单位"""
62
+ if self.unit == 'MB':
63
+ return bytes_value / (1024 * 1024)
64
+ else: # GB
65
+ return bytes_value / (1024 * 1024 * 1024)
66
+
67
+ def _get_current_usage(self):
68
+ """获取当前显存使用量(按指定单位)"""
69
+ mem_info = self._get_memory_info()
70
+ if mem_info:
71
+ return self._convert_units(mem_info.used)
72
+ return 0
73
+
74
+ def _monitor_loop(self):
75
+ """监控循环,运行在单独线程中"""
76
+ # print(f"开始监控 GPU {self.gpu_index} ({self.gpu_name})")
77
+ # print(f"监控间隔: {self.interval}秒")
78
+ # print(f"单位: {self.unit}")
79
+ # print("按回车键停止监控...\n")
80
+
81
+ try:
82
+ while self._running:
83
+ # 获取当前显存使用量
84
+ current_usage = self._get_current_usage()
85
+
86
+ # 更新峰值
87
+ with self._lock:
88
+ if current_usage > self.peak_memory_usage:
89
+ self.peak_memory_usage = current_usage
90
+
91
+ # 等待下一个监控周期
92
+ time.sleep(self.interval)
93
+
94
+ except Exception as e:
95
+ print(f"监控线程出错: {e}")
96
+ finally:
97
+ # print(f"GPU {self.gpu_index} 监控线程结束")
98
+ pass
99
+
100
+ def start(self):
101
+ """
102
+ 启动显存监控
103
+
104
+ Returns:
105
+ bool: 是否成功启动
106
+ """
107
+ if self._running:
108
+ # print(f"GPU {self.gpu_index} 监控已经在运行中")
109
+ return False
110
+
111
+ try:
112
+ # 重置峰值数据
113
+ with self._lock:
114
+ self.peak_memory_usage = 0
115
+
116
+ # 记录开始时间
117
+ self.start_time = datetime.now()
118
+
119
+ # 启动监控线程
120
+ self._running = True
121
+ self.monitor_thread = threading.Thread(target=self._monitor_loop)
122
+ self.monitor_thread.daemon = True
123
+ self.monitor_thread.start()
124
+
125
+ # 验证线程已启动
126
+ time.sleep(0.1)
127
+ if self.monitor_thread.is_alive():
128
+ # print(f"GPU {self.gpu_index} 监控已启动")
129
+ return True
130
+ else:
131
+ self._running = False
132
+ # print(f"GPU {self.gpu_index} 监控线程启动失败")
133
+ return False
134
+
135
+ except Exception as e:
136
+ print(f"启动GPU {self.gpu_index} 监控失败: {e}")
137
+ self._running = False
138
+ return False
139
+
140
+ def stop(self, verbose=False):
141
+ """
142
+ 停止显存监控并返回峰值显存使用量
143
+
144
+ Args:
145
+ verbose: 是否打印详细信息
146
+
147
+ Returns:
148
+ float: 峰值显存使用量(单位:GB或MB)
149
+ """
150
+ if not self._running:
151
+ if verbose:
152
+ print(f"GPU {self.gpu_index} 监控未在运行")
153
+ return 0.0
154
+
155
+ try:
156
+ # 停止监控线程
157
+ self._running = False
158
+ self.stop_time = datetime.now()
159
+
160
+ # 等待线程结束(最多等待2秒)
161
+ if self.monitor_thread and self.monitor_thread.is_alive():
162
+ self.monitor_thread.join(timeout=2.0)
163
+
164
+ # 获取最终峰值
165
+ peak_usage = 0.0
166
+ with self._lock:
167
+ peak_usage = self.peak_memory_usage
168
+
169
+ if verbose:
170
+ duration = (self.stop_time - self.start_time).total_seconds()
171
+ print(f"\n{'='*50}")
172
+ print(f"GPU {self.gpu_index} ({self.gpu_name}) 监控结果")
173
+ print(f"{'='*50}")
174
+ print(f"开始时间: {self.start_time.strftime('%Y-%m-%d %H:%M:%S')}")
175
+ print(f"结束时间: {self.stop_time.strftime('%Y-%m-%d %H:%M:%S')}")
176
+ print(f"监控时长: {duration:.1f} 秒")
177
+ print(f"峰值显存使用量: {peak_usage:.3f} {self.unit}")
178
+
179
+ # 获取当前显存信息作为对比
180
+ mem_info = self._get_memory_info()
181
+ if mem_info:
182
+ total = self._convert_units(mem_info.total)
183
+ current = self._convert_units(mem_info.used)
184
+ print(f"当前显存使用: {current:.3f} / {total:.3f} {self.unit}")
185
+ print(f"峰值占比: {(peak_usage/total*100):.1f}%")
186
+ print(f"{'='*50}")
187
+
188
+ return peak_usage
189
+
190
+ except Exception as e:
191
+ print(f"停止GPU {self.gpu_index} 监控时出错: {e}")
192
+ return 0.0
193
+
194
+ def get_current_usage(self):
195
+ """获取当前显存使用量(不停止监控)"""
196
+ return self._get_current_usage()
197
+
198
+ def get_peak_usage(self):
199
+ """获取当前记录的峰值显存使用量(不停止监控)"""
200
+ with self._lock:
201
+ return self.peak_memory_usage
202
+
203
+ def is_running(self):
204
+ """检查监控是否在运行"""
205
+ return self._running
206
+
207
+ def __del__(self):
208
+ """析构函数,确保清理资源"""
209
+ if self._running:
210
+ self.stop(verbose=False)
211
+ try:
212
+ pynvml.nvmlShutdown()
213
+ except:
214
+ pass
215
+
216
+
217
+ # 使用示例函数
218
+ def monitor_gpu_memory_example():
219
+ """使用示例"""
220
+ print("GPU显存监控示例")
221
+ print("=" * 50)
222
+
223
+ try:
224
+ # 创建监控器
225
+ monitor = GPUMemoryMonitor(
226
+ gpu_index=0, # 监控第一个GPU
227
+ interval=0.5, # 每0.5秒检查一次
228
+ unit='GB' # 使用GB作为单位
229
+ )
230
+
231
+ # 启动监控
232
+ if monitor.start():
233
+ # 这里可以运行你的GPU任务
234
+ print("\n现在开始运行你的GPU任务...")
235
+ print("监控正在后台进行")
236
+
237
+ # 模拟一些工作(在实际使用中,这里应该是你的GPU任务)
238
+ print("按回车键停止监控并获取峰值显存用量...")
239
+ input() # 等待用户按回车
240
+
241
+ # 停止监控并获取结果
242
+ peak_memory_gb = monitor.stop()
243
+
244
+ print(f"\n监控完成!峰值显存用量: {peak_memory_gb:.3f} GB")
245
+
246
+ except Exception as e:
247
+ print(f"错误: {e}")
248
+
249
+
250
+ # 多GPU监控示例
251
+ class MultiGPUMonitor:
252
+ """多GPU监控器"""
253
+ def __init__(self, gpu_indices=None, interval=1.0, unit='GB'):
254
+ """
255
+ 初始化多GPU监控器
256
+
257
+ Args:
258
+ gpu_indices: 要监控的GPU索引列表,None表示监控所有GPU
259
+ interval: 监控间隔
260
+ unit: 返回的单位
261
+ """
262
+ pynvml.nvmlInit()
263
+ device_count = pynvml.nvmlDeviceGetCount()
264
+
265
+ if gpu_indices is None:
266
+ gpu_indices = list(range(device_count))
267
+
268
+ self.monitors = []
269
+ for idx in gpu_indices:
270
+ if idx < device_count:
271
+ monitor = GPUMemoryMonitor(gpu_index=idx, interval=interval, unit=unit)
272
+ self.monitors.append(monitor)
273
+ else:
274
+ print(f"警告: GPU索引 {idx} 不存在,跳过")
275
+
276
+ def start_all(self):
277
+ """启动所有GPU监控"""
278
+ results = []
279
+ for monitor in self.monitors:
280
+ success = monitor.start()
281
+ results.append((monitor.gpu_index, success))
282
+ return results
283
+
284
+ def stop_all(self):
285
+ """停止所有GPU监控并返回结果"""
286
+ results = {}
287
+ max_peak = 0.0
288
+ for monitor in self.monitors:
289
+ peak = monitor.stop()
290
+ results[monitor.gpu_index] = {
291
+ 'name': monitor.gpu_name,
292
+ 'peak_memory': peak,
293
+ 'unit': monitor.unit
294
+ }
295
+ if peak > max_peak:
296
+ max_peak = peak
297
+ return results, max_peak
298
+
299
+ def stop_all_and_get_max(self):
300
+ """停止所有监控并返回最大峰值"""
301
+ results = self.stop_all()
302
+ if not results:
303
+ return 0.0
304
+
305
+ max_peak = max(item['peak_memory'] for item in results.values())
306
+ return max_peak
307
+
308
+
309
+ # 快速使用函数
310
+ def quick_monitor(gpu_index=0, interval=0.5, unit='GB', wait_for_input=True):
311
+ """
312
+ 快速启动监控的便捷函数
313
+
314
+ Args:
315
+ gpu_index: GPU索引
316
+ interval: 监控间隔
317
+ unit: 单位
318
+ wait_for_input: 是否等待用户输入
319
+
320
+ Returns:
321
+ float: 峰值显存使用量
322
+ """
323
+ monitor = GPUMemoryMonitor(gpu_index=gpu_index, interval=interval, unit=unit)
324
+
325
+ try:
326
+ if monitor.start():
327
+ if wait_for_input:
328
+ print("按回车键停止监控...")
329
+ input()
330
+ else:
331
+ # 如果不等待用户输入,这里可以设置其他停止条件
332
+ # 例如:监控特定时间或直到某个条件满足
333
+ print("监控已启动,将在后台运行")
334
+ print("调用 monitor.stop() 来停止并获取结果")
335
+ return monitor
336
+ return None
337
+ except Exception as e:
338
+ print(f"监控失败: {e}")
339
+ return None
340
+
341
+
342
+ # 测试代码
343
+ if __name__ == "__main__":
344
+ # # 示例1: 基本使用
345
+ # print("示例1: 基本使用")
346
+ # monitor = GPUMemoryMonitor(gpu_index=0, interval=0.5, unit='GB')
347
+ # monitor.start()
348
+
349
+ # # 模拟一些GPU工作
350
+ # print("模拟GPU工作...")
351
+ # time.sleep(3)
352
+
353
+ # # 停止并获取结果
354
+ # peak = monitor.stop()
355
+ # print(f"峰值显存: {peak:.3f} GB\n")
356
+
357
+ # # 示例2: 使用便捷函数
358
+ # print("示例2: 使用便捷函数")
359
+ # result = quick_monitor(gpu_index=0, interval=0.2, unit='MB')
360
+ # if result:
361
+ # # 这里 result 是 monitor 对象
362
+ # time.sleep(2)
363
+ # peak = result.stop()
364
+ # print(f"峰值显存: {peak:.3f} MB")
365
+
366
+ # 示例3: 多GPU监控
367
+ print("\n示例3: 多GPU监控")
368
+ try:
369
+ pynvml.nvmlInit()
370
+ device_count = pynvml.nvmlDeviceGetCount()
371
+ print(f"系统中有 {device_count} 个GPU")
372
+
373
+ if device_count > 1:
374
+ # multi_monitor = MultiGPUMonitor(gpu_indices=[0, 1], interval=0.5, unit='GB')
375
+ multi_monitor = MultiGPUMonitor(interval=5, unit='GB')
376
+ multi_monitor.start_all()
377
+ time.sleep(2)
378
+ results, max_peak = multi_monitor.stop_all()
379
+
380
+ for gpu_idx, data in results.items():
381
+ print(f"GPU {gpu_idx} ({data['name']}): {data['peak_memory']:.3f} {data['unit']}")
382
+ print(f"最大峰值显存使用量: {max_peak:.3f} GB")
383
+ except:
384
+ pass
src/utils/gpu_worker.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import torch
3
+
4
+ class GpuWorker:
5
+
6
+ def __init__(self, gpu_id: int, envs: dict):
7
+ self.gpu_id = gpu_id
8
+ self._setup_environment(envs)
9
+ self.device = self._setup_device()
10
+
11
+ def _setup_environment(self, envs):
12
+ """设置环境变量"""
13
+ for k, v in envs.items():
14
+ os.environ[k] = v
15
+
16
+ def _setup_device(self):
17
+ """设置CUDA设备"""
18
+ torch.cuda.set_device(self.gpu_id)
19
+ return f"cuda:{self.gpu_id}"
src/utils/misc.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import subprocess
3
+ import math
4
+ import torch
5
+ import torch.distributed as dist
6
+ import transformers
7
+ from typing import Union, Iterable, List, Dict, Tuple, Optional
8
+
9
+ class DictWithDotAccess(dict):
10
+ def __init__(self, *args, **kwargs):
11
+ super().__init__(*args, **kwargs)
12
+ for arg in args:
13
+ if isinstance(arg, dict):
14
+ for key, value in arg.items():
15
+ if isinstance(value, dict):
16
+ value = DictWithDotAccess(value)
17
+ self[key] = value
18
+
19
+ if kwargs:
20
+ for key, value in kwargs.items():
21
+ if isinstance(value, dict):
22
+ value = DictWithDotAccess(value)
23
+ self[key] = value
24
+
25
+ def __getattr__(self, attr):
26
+ return self.get(attr)
27
+
28
+
29
+ def is_dist_avail_and_initialized():
30
+ if not dist.is_available():
31
+ return False
32
+ if not dist.is_initialized():
33
+ return False
34
+ return True
35
+
36
+
37
+ def get_rank():
38
+ if not is_dist_avail_and_initialized():
39
+ return 0
40
+ return dist.get_rank()
41
+
42
+ def get_local_rank():
43
+ if not is_dist_avail_and_initialized():
44
+ return 0
45
+ return int(os.environ["LOCAL_RANK"])
46
+
47
+ def Print(*args):
48
+ if get_rank() == 0:
49
+ print(*args)
50
+
51
+ def get_sha():
52
+ cwd = os.path.dirname(os.path.abspath(__file__))
53
+
54
+ def _run(command):
55
+ return subprocess.check_output(command, cwd=cwd).decode("ascii").strip()
56
+
57
+ sha = "N/A"
58
+ diff = "clean"
59
+ branch = "N/A"
60
+ try:
61
+ sha = _run(["git", "rev-parse", "HEAD"])
62
+ subprocess.check_output(["git", "diff"], cwd=cwd)
63
+ diff = _run(["git", "diff-index", "HEAD"])
64
+ diff = "has uncommited changes" if diff else "clean"
65
+ branch = _run(["git", "rev-parse", "--abbrev-ref", "HEAD"])
66
+ except Exception:
67
+ pass
68
+ message = f"sha: {sha}, status: {diff}, branch: {branch}"
69
+ return message
70
+
71
+ def patch_cosine_with_warmup_schedule(minimal_lr=0.0):
72
+ def _get_cosine_schedule_with_warmup_lr_lambda(
73
+ current_step: int, *, num_warmup_steps: int, num_training_steps: int, num_cycles: float
74
+ ):
75
+ if current_step < num_warmup_steps:
76
+ return float(current_step) / float(max(1, num_warmup_steps))
77
+ progress = float(current_step - num_warmup_steps) / float(max(1, num_training_steps - num_warmup_steps))
78
+ return max(minimal_lr, 0.5 * (1.0 + math.cos(math.pi * float(num_cycles) * 2.0 * progress)))
79
+
80
+ transformers.optimization._get_cosine_schedule_with_warmup_lr_lambda = _get_cosine_schedule_with_warmup_lr_lambda
81
+
82
+
83
+ # patch torch clip grad norm so that we can skip nan grad norm
84
+ from torch import Tensor, inf
85
+ from torch.utils._foreach_utils import _group_tensors_by_device_and_dtype, _has_foreach_support
86
+ _tensor_or_tensors = Union[torch.Tensor, Iterable[torch.Tensor]]
87
+ def clip_grad_norm_(
88
+ parameters: _tensor_or_tensors, max_norm: float, norm_type: float = 2.0,
89
+ error_if_nonfinite: bool = False, foreach: Optional[bool] = None) -> torch.Tensor:
90
+ r"""Clips gradient norm of an iterable of parameters.
91
+
92
+ The norm is computed over all gradients together, as if they were
93
+ concatenated into a single vector. Gradients are modified in-place.
94
+
95
+ Args:
96
+ parameters (Iterable[Tensor] or Tensor): an iterable of Tensors or a
97
+ single Tensor that will have gradients normalized
98
+ max_norm (float): max norm of the gradients
99
+ norm_type (float): type of the used p-norm. Can be ``'inf'`` for
100
+ infinity norm.
101
+ error_if_nonfinite (bool): if True, an error is thrown if the total
102
+ norm of the gradients from :attr:`parameters` is ``nan``,
103
+ ``inf``, or ``-inf``. Default: False (will switch to True in the future)
104
+ foreach (bool): use the faster foreach-based implementation.
105
+ If ``None``, use the foreach implementation for CUDA and CPU native tensors and silently
106
+ fall back to the slow implementation for other device types.
107
+ Default: ``None``
108
+
109
+ Returns:
110
+ Total norm of the parameter gradients (viewed as a single vector).
111
+ """
112
+ if isinstance(parameters, torch.Tensor):
113
+ parameters = [parameters]
114
+ grads = [p.grad for p in parameters if p.grad is not None]
115
+ max_norm = float(max_norm)
116
+ norm_type = float(norm_type)
117
+ if len(grads) == 0:
118
+ return torch.tensor(0.)
119
+
120
+ if torch.isnan(max_norm) or torch.isinf(max_norm):
121
+ for grad in grads:
122
+ grad.zero_()
123
+ print('>>>Found nan or inf max_norm, set grads to zero')
124
+ return torch.tensor(0.)
125
+
126
+ first_device = grads[0].device
127
+ grouped_grads: Dict[Tuple[torch.device, torch.dtype], List[List[Tensor]]] \
128
+ = _group_tensors_by_device_and_dtype([[g.detach() for g in grads]]) # type: ignore[assignment]
129
+
130
+ if norm_type == inf:
131
+ norms = [g.detach().abs().max().to(first_device) for g in grads]
132
+ total_norm = norms[0] if len(norms) == 1 else torch.max(torch.stack(norms))
133
+ else:
134
+ norms = []
135
+ for ((device, _), [grads]) in grouped_grads.items():
136
+ if (foreach is None or foreach) and _has_foreach_support(grads, device=device):
137
+ norms.extend(torch._foreach_norm(grads, norm_type))
138
+ elif foreach:
139
+ raise RuntimeError(f'foreach=True was passed, but can\'t use the foreach API on {device.type} tensors')
140
+ else:
141
+ norms.extend([torch.norm(g, norm_type) for g in grads])
142
+
143
+ total_norm = torch.norm(torch.stack([norm.to(first_device) for norm in norms]), norm_type)
144
+
145
+ if torch.isnan(total_norm) or torch.isinf(total_norm):
146
+ for grad in grads:
147
+ grad.zero_()
148
+ print('>>>Found nan or inf total_norm, set grads to zero')
149
+ return torch.tensor(0.)
150
+
151
+ if error_if_nonfinite and torch.logical_or(total_norm.isnan(), total_norm.isinf()):
152
+ raise RuntimeError(
153
+ f'The total norm of order {norm_type} for gradients from '
154
+ '`parameters` is non-finite, so it cannot be clipped. To disable '
155
+ 'this error and scale the gradients by the non-finite norm anyway, '
156
+ 'set `error_if_nonfinite=False`')
157
+ clip_coef = max_norm / (total_norm + 1e-6)
158
+ # Note: multiplying by the clamped coef is redundant when the coef is clamped to 1, but doing so
159
+ # avoids a `if clip_coef < 1:` conditional which can require a CPU <=> device synchronization
160
+ # when the gradients do not reside in CPU memory.
161
+ clip_coef_clamped = torch.clamp(clip_coef, max=1.0)
162
+ for ((device, _), [grads]) in grouped_grads.items():
163
+ if (foreach is None or foreach) and _has_foreach_support(grads, device=device):
164
+ torch._foreach_mul_(grads, clip_coef_clamped.to(device)) # type: ignore[call-overload]
165
+ elif foreach:
166
+ raise RuntimeError(f'foreach=True was passed, but can\'t use the foreach API on {device.type} tensors')
167
+ else:
168
+ clip_coef_clamped_device = clip_coef_clamped.to(device)
169
+ for g in grads:
170
+ g.detach().mul_(clip_coef_clamped_device)
171
+
172
+ return total_norm
173
+
174
+ def patch_torch_clip_grad_norm():
175
+ torch.nn.utils.clip_grad_norm_ = clip_grad_norm_
src/utils/resave_model.py ADDED
@@ -0,0 +1,70 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ sys.path.append(os.getcwd())
4
+ from src.msa.model import MSAForCausalLM
5
+ from src.msa.configuration_msa import MSAConfig
6
+ from src.utils.common import print_model_stats
7
+ from transformers import AutoTokenizer
8
+
9
+ def save_checkpoint(model, tokenizer, save_model_path):
10
+ model.save_pretrained(save_model_path)
11
+ tokenizer.save_pretrained(save_model_path)
12
+
13
+ def main(origin_model_path, save_model_path):
14
+ router_layer_idx = os.environ.get("ROUTER_LAYER_IDX", "all")
15
+ aux_loss = os.environ.get("AUX_LOSS", "false") == "true"
16
+ lmloss_weigth = float(os.environ.get("LMLOSS_WEIGHT", 1.0))
17
+ auxloss_weight = float(os.environ.get("AUX_LOSS_WEIGHT", 0.1))
18
+ recloss_weight = float(os.environ.get("REC_LOSS_WEIGHT", 0.0))
19
+ ansloss_weight = float(os.environ.get("ANS_LOSS_WEIGHT", 1.0))
20
+ aux_loss_method = os.environ.get("AUX_LOSS_METHOD", "INFONCE") # INFONCE, BCE, INFONCE_DECOUPLE, INFONCE_DECOUPLE_FOCAL
21
+ decouple_router = os.environ.get("DECOUPLE_ROUTER", "false").lower() == "true"
22
+ rewrite_position = os.environ.get("REWRITE_POSITION", "false") == "true"
23
+
24
+ top_k_docs = int(os.environ.get("TOP_K_DOCS", 2))
25
+ pooling_kernel_size = int(os.environ.get("POOLING_KERNEL_SIZE", 2))
26
+
27
+ head_reduce_method = os.environ.get("HEAD_REDUCE_METHOD", "max")
28
+ query_reduce_method = os.environ.get("QUERY_REDUCE_METHOD", "max")
29
+ chunk_reduce_method = os.environ.get("CHUNK_REDUCE_METHOD", "max")
30
+ decouple_pooling_mode = os.environ.get("DECOUPLE_POOLING_MODE", "mean")
31
+ infonce_loss_temp = float(os.environ.get("INFONCE_LOSS_TEMP", 0.1))
32
+
33
+ msa_config = {
34
+ "router_layer_idx": router_layer_idx,
35
+ "aux_loss": aux_loss,
36
+ "lmloss_weigth": lmloss_weigth,
37
+ "auxloss_weight": auxloss_weight,
38
+ "recloss_weight": recloss_weight,
39
+ "ansloss_weight": ansloss_weight,
40
+ "aux_loss_method": aux_loss_method,
41
+ "decouple_router": decouple_router,
42
+ "rewrite_position": rewrite_position,
43
+ "top_k_docs": top_k_docs,
44
+ "pooling_kernel_size": pooling_kernel_size,
45
+ "infonce_loss_temp": infonce_loss_temp,
46
+ "head_reduce_method": head_reduce_method,
47
+ "query_reduce_method": query_reduce_method,
48
+ "chunk_reduce_method": chunk_reduce_method,
49
+ "decouple_pooling_mode": decouple_pooling_mode,
50
+ }
51
+ # 使用 MSAConfig,它会自动将 msa_config 转换为 DotDict
52
+ config = MSAConfig.from_pretrained(origin_model_path)
53
+ config.msa_config = msa_config # MSAConfig 会自动转换为 DotDict
54
+ tokenizer = AutoTokenizer.from_pretrained(origin_model_path)
55
+ model = MSAForCausalLM.from_pretrained(
56
+ origin_model_path,
57
+ config=config,
58
+ torch_dtype="bfloat16",
59
+ )
60
+ print_model_stats(model)
61
+
62
+ # save
63
+ save_checkpoint(model, tokenizer, save_model_path)
64
+
65
+ if __name__ == "__main__":
66
+ origin_model_path = sys.argv[1]
67
+ save_model_path = sys.argv[2]
68
+ main(origin_model_path, save_model_path)
69
+ print(f"Model has been saved to : {save_model_path}")
70
+ print("Done")
src/utils/scale.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import pickle
3
+
4
+ def scale_memory(context, query, scale=3):
5
+ with open(query, "rb") as f:
6
+ query_metas = pickle.load(f)
7
+
8
+ labels_contents = [ref for q_meta in query_metas for ref in q_meta['reference_list']]
9
+ copy_reference = [f"{ref}_copy{bias}" for bias, ref in enumerate(context * scale) if ref not in labels_contents]
10
+ context.extend(copy_reference)
src/utils/template.py ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ QWEN3_TEMPLATE={
2
+ "prompt": "<|im_start|>user\n{prompt}<|im_end|>\n",
3
+ "response": "<|im_start|>assistant\n<think>\n{think_content}\n</think>\n\n{output}<|im_end|>"
4
+ }
5
+
6
+ QWEN3_INSTRUCT_TEMPLATE={
7
+ "prompt": "<|im_start|>user\n{prompt}<|im_end|>\n",
8
+ "response": "<|im_start|>assistant\n{output}<|im_end|>"
9
+ }
src/utils/tools.py ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ import torch
3
+ import threading
4
+
5
+ def format_bytes(size_in_bytes):
6
+ """
7
+ 将字节数转换为人类可读的格式
8
+ 单位为 M 及以下不要小数点,单位为 G 以上保留一位小数,数值不能小于 1
9
+ """
10
+ # 定义单位
11
+ units = ['B', 'K', 'M', 'G', 'T', 'P']
12
+
13
+ # 处理边界情况
14
+ if size_in_bytes < 1:
15
+ return "0B"
16
+
17
+ # 计算单位索引
18
+ unit_index = 0
19
+ size = float(size_in_bytes)
20
+
21
+ while size >= 1024 and unit_index < len(units) - 1:
22
+ size /= 1024
23
+ unit_index += 1
24
+
25
+ # 根据单位决定格式化方式
26
+ if unit_index <= 2: # B, K, M 不要小数点
27
+ if size == int(size):
28
+ return f"{int(size)}{units[unit_index]}"
29
+ else:
30
+ return f"{int(round(size))}{units[unit_index]}"
31
+ else: # G 及以上保留一位小数
32
+ return f"{size:.1f}{units[unit_index]}"
33
+
34
+ def cumulative_concat(tensors):
35
+ # 一次性获取所有信息
36
+ lengths = [len(t) for t in tensors]
37
+ last_values = [t[-1] for t in tensors]
38
+
39
+ # 计算累积偏移(不包括最后一个tensor)
40
+ cum_offsets = torch.cumsum(torch.tensor([0] + last_values[:-1]), dim=0)
41
+
42
+ # 构建偏移数组
43
+ total_length = sum(lengths)
44
+ offsets = torch.zeros(total_length, dtype=tensors[0].dtype, device=tensors[0].device)
45
+
46
+ # 为每个tensor设置对应的偏移
47
+ start_idx = 0
48
+ for i, length in enumerate(lengths):
49
+ if i > 0: # 第一个tensor不需要偏移
50
+ offsets[start_idx:start_idx + length] = cum_offsets[i]
51
+ start_idx += length
52
+
53
+ # 一次性拼接和添加偏移
54
+ concatenated = torch.cat(tensors)
55
+ return concatenated + offsets
56
+
57
+
58
+ class RequestLimiter:
59
+ def __init__(self, max_concurrent=10):
60
+ """
61
+ 初始化请求限流器
62
+
63
+ Args:
64
+ max_concurrent: 最大并发请求数,默认10
65
+ """
66
+ self.max_concurrent = max_concurrent
67
+ self.current_count = 0
68
+ self.lock = threading.Lock()
69
+ self.condition = threading.Condition(self.lock)
70
+
71
+ def acquire(self):
72
+ """
73
+ 获取执行权限,如果超过最大并发数则阻塞
74
+
75
+ Returns:
76
+ bool: 是否成功获取权限
77
+ """
78
+ with self.lock:
79
+ while self.current_count >= self.max_concurrent:
80
+ # 等待有请求完成
81
+ self.condition.wait()
82
+ self.current_count += 1
83
+ return True
84
+
85
+ def release(self):
86
+ """
87
+ 释放一个执行权限,唤醒等待的请求
88
+ """
89
+ with self.lock:
90
+ if self.current_count > 0:
91
+ self.current_count -= 1
92
+ # 通知一个等待的线程
93
+ self.condition.notify()
94
+
95
+ def compose_input(doc, doc_idx, tokenizer):
96
+ """组建reference的 input"""
97
+ new_doc = "<|im_start|>" + f"[{doc_idx}]. {doc}[{doc_idx}]<|im_end|>"
98
+ return new_doc,tokenizer(new_doc, add_special_tokens=False)
99
+
100
+
101
+ class TimePoint:
102
+ def __init__(self, disabled=False):
103
+ self.pts = []
104
+ self.disabled = disabled
105
+
106
+ def add(self, name):
107
+ if not self.disabled:
108
+ self.pts.append((name, time.time()))
109
+
110
+ def print(self):
111
+ if len(self.pts) < 2:
112
+ return
113
+
114
+ total = self.pts[-1][1] - self.pts[0][1]
115
+ s = ""
116
+ if len(self.pts) > 2:
117
+ lst1 = self.pts[:-1]
118
+ lst2 = self.pts[1:]
119
+ s = " | ".join(f"{item1[0]}->{item2[0]}: {item2[1] - item1[1]:.3f}" for item1, item2 in zip(lst1, lst2))
120
+ print(f"total: {total:.2f} {s}")