MindscapeRAG commited on
Commit
8534b94
·
verified ·
1 Parent(s): 091400d

Update README.md

Browse files

Add environment/dependencies, document layer-truncation architecture

Files changed (1) hide show
  1. README.md +156 -315
README.md CHANGED
@@ -30,7 +30,6 @@ tags:
30
 
31
  QRRanker is a lightweight reranking framework that leverages **Query-focused Retrieval (QR) heads** to produce continuous relevance scores, enabling effective listwise reranking with small-scale models.
32
 
33
-
34
  ## Model Description
35
 
36
  Built upon the existing analysis of retrieval heads in large language models, QRRanker trains models to estimate passage–query relevance using the attention scores of selected **Query-focused Retrieval (QR) heads**. These heads are identified through QR score computation on seed data and are particularly effective at capturing query-document relevance signals.
@@ -42,293 +41,158 @@ Our approach provides a **listwise solution** that leverages the holistic inform
42
  - **Listwise Reranking**: Leverages holistic information within the entire candidate shortlist during ranking
43
  - **Continuous Relevance Scores**: Enables training on arbitrary retrieval datasets without requiring Likert-scale supervision
44
  - **Selective Head Usage**: Focuses on top-performing QR attention heads
 
45
  - **Memory Enhancement**: Optional contextual summaries for improved accuracy on long narratives and dialogues
46
 
47
- ## Quick Start
48
 
 
49
 
50
- ### Basic Usage
51
 
52
- ```python
53
- import torch
54
- from transformers import AutoModel, AutoConfig, AutoTokenizer
 
55
 
56
- # Load model
57
- config = AutoConfig.from_pretrained("MindscapeRAG/QRRanker", trust_remote_code=True)
58
- model = AutoModel.from_pretrained(
59
- "MindscapeRAG/QRRanker",
60
- config=config,
61
- torch_dtype=torch.float16,
62
- trust_remote_code=True,
63
- )
64
- model.eval()
65
 
66
- # Load tokenizer
67
- tokenizer = AutoTokenizer.from_pretrained("MindscapeRAG/QRRanker", trust_remote_code=True)
68
  ```
69
-
70
- ## Input Data Format
71
-
72
- Input data should be in JSON format. Each sample contains the following fields:
73
-
74
- ```json
75
- {
76
- "id": "sample_001",
77
- "question": "What is the capital of France?",
78
- "answer": "Paris",
79
- "paragraphs": [
80
- {
81
- "idx": 0,
82
- "title": "France",
83
- "paragraph_text": "Paris is the capital and largest city of France...",
84
- "is_supporting": true
85
- },
86
- {
87
- "idx": 1,
88
- "title": "Germany",
89
- "paragraph_text": "Berlin is the capital of Germany...",
90
- "is_supporting": false
91
- }
92
- ],
93
- "summary": "Optional summary text..."
94
- }
95
  ```
96
 
97
- ### Field Description
98
-
99
- | Field | Type | Required | Description |
100
- |-------|------|----------|-------------|
101
- | `id` | string | Yes | Unique sample identifier |
102
- | `question` | string | Yes | User query/question |
103
- | `answer` | string | No | Ground truth answer (for evaluation) |
104
- | `paragraphs` | list | Yes | List of candidate paragraphs |
105
- | `paragraphs[].idx` | int | Yes | Paragraph index |
106
- | `paragraphs[].title` | string | No | Paragraph title |
107
- | `paragraphs[].paragraph_text` | string | Yes | Paragraph content |
108
- | `paragraphs[].is_supporting` | bool | No | Whether it's a supporting paragraph (for evaluation) |
109
- | `summary` | string | No | Optional summary information |
110
 
111
- ## Core Algorithm
112
 
113
- ### 0. DynamicCacheWithQuery (Custom Cache Class)
 
 
 
 
 
 
 
 
114
 
115
- This custom cache class is essential for QRRanker. It extends the standard `DynamicCache` to also store query states at specified positions.
116
 
 
117
 
118
  ```python
119
- from typing import Any, Dict, Optional, Tuple
120
- from transformers.cache_utils import DynamicCache
121
  import torch
 
122
 
 
 
 
 
 
 
 
 
 
123
 
124
- class DynamicCacheWithQuery(DynamicCache):
125
- """
126
- Custom cache class for QRRanker that stores both key/value states and query states.
127
- The query states are extracted at specified token positions for attention computation.
128
- """
129
-
130
- def __init__(self, query_indices=[]) -> None:
131
- super().__init__()
132
- self._query_indices = query_indices # Token indices where query states should be saved
133
- self.query_cache = []
134
-
135
- def update(
136
- self,
137
- key_states: torch.Tensor,
138
- value_states: torch.Tensor,
139
- layer_idx: int,
140
- cache_kwargs: Optional[Dict[str, Any]] = None,
141
- ) -> Tuple[torch.Tensor, torch.Tensor]:
142
- """
143
- Updates the cache with new key_states, value_states, and optionally query_states.
144
-
145
- Parameters:
146
- key_states: New key states to cache [batch, num_kv_heads, seq_len, head_dim]
147
- value_states: New value states to cache [batch, num_kv_heads, seq_len, head_dim]
148
- layer_idx: Index of the layer
149
- cache_kwargs: Optional dict containing 'query_states' to cache
150
-
151
- Returns:
152
- Tuple of (updated_key_states, updated_value_states)
153
- """
154
- # Update seen tokens count
155
- if layer_idx == 0:
156
- self._seen_tokens += key_states.shape[-2]
157
-
158
- # Update key/value cache
159
- if key_states is not None:
160
- if len(self.key_cache) <= layer_idx:
161
- for _ in range(len(self.key_cache), layer_idx):
162
- self.key_cache.append(torch.tensor([]))
163
- self.value_cache.append(torch.tensor([]))
164
- self.key_cache.append(key_states)
165
- self.value_cache.append(value_states)
166
- elif not self.key_cache[layer_idx].numel():
167
- self.key_cache[layer_idx] = key_states
168
- self.value_cache[layer_idx] = value_states
169
- else:
170
- self.key_cache[layer_idx] = torch.cat(
171
- [self.key_cache[layer_idx], key_states], dim=-2
172
- )
173
- self.value_cache[layer_idx] = torch.cat(
174
- [self.value_cache[layer_idx], value_states], dim=-2
175
- )
176
-
177
- # Update query cache if query_states provided
178
- if cache_kwargs is not None:
179
- query_states = cache_kwargs.get("query_states", None)
180
- else:
181
- query_states = None
182
-
183
- if query_states is not None:
184
- if len(self.query_cache) <= layer_idx:
185
- self.query_cache.append(query_states)
186
- else:
187
- self.query_cache[layer_idx] = torch.cat(
188
- [self.query_cache[layer_idx], query_states], dim=-2
189
- )
190
-
191
- return self.key_cache[layer_idx], self.value_cache[layer_idx]
192
  ```
193
 
194
- ### 1. Attention Weight Computation
 
 
195
 
196
  ```python
197
  import math
198
- import torch
199
 
200
- def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
201
- """Expand key/value states to match the number of query heads."""
202
- batch, num_key_value_heads, slen, head_dim = hidden_states.shape
203
  if n_rep == 1:
204
  return hidden_states
205
- hidden_states = hidden_states[:, :, None, :, :].expand(
206
- batch, num_key_value_heads, n_rep, slen, head_dim
207
- )
208
- return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
209
-
210
-
211
- def get_causal_mask(attn_weights):
212
- """Generate causal attention mask."""
213
- query_len, seq_len = attn_weights.size(-2), attn_weights.size(-1)
214
- causal_mask = torch.ones_like(attn_weights.transpose(-1, -2).squeeze(0))
215
- causal_mask = torch.triu(causal_mask, diagonal=-(seq_len - query_len))
216
- causal_mask = causal_mask.transpose(-1, -2)
217
- causal_mask = (1 - causal_mask) * torch.finfo(causal_mask.dtype).min
218
- return causal_mask
219
 
220
 
221
  def get_attn_weights(key_states, query_states):
222
- """Compute attention weights between query and key states."""
223
  bsz, num_heads, q_len, head_dim = query_states.size()
224
- num_key_value_heads = key_states.size(1)
225
- num_key_value_groups = num_heads // num_key_value_heads
226
- kv_seq_len = key_states.size(-2)
227
-
228
- # Expand key states to match query heads
229
- key_states = repeat_kv(key_states, num_key_value_groups)
230
 
231
- # Scaled dot-product attention
232
  scale = 1.0 / math.sqrt(head_dim)
233
- scaled_queries = query_states * scale
234
- attn_weights = torch.matmul(scaled_queries, key_states.transpose(2, 3))
235
-
236
- # Apply causal mask
237
- causal_mask = get_causal_mask(attn_weights).to(attn_weights.device)
238
- attn_weights += causal_mask.unsqueeze(0)
239
-
240
- # Softmax normalization
241
  attn_lses = torch.logsumexp(attn_weights, dim=-1, keepdim=True)
242
- attn_weights = torch.exp(attn_weights - attn_lses)
243
-
244
- return attn_weights
245
- ```
246
 
247
- ### 2. QRRanker Score Computation
248
 
249
- ```python
250
- def compute_qr_scores(
251
- query_cache,
252
- key_cache,
253
- qr_head_list,
254
- chunk_ranges,
255
- query_upper_bound,
256
- ):
257
  """
258
- Compute QRRanker attention scores for document chunks.
259
-
260
  Args:
261
- query_cache: List of query states from each layer
262
- key_cache: List of key states from each layer
263
- qr_head_list: String of QR heads, e.g., "20-15,21-11,17-27,..."
264
- chunk_ranges: List of [start, end] token positions for each chunk
265
- query_upper_bound: Upper bound token position for query
266
-
267
  Returns:
268
- scores: Tensor of shape [num_chunks] with relevance scores
269
  """
270
  all_head_scores = []
271
-
272
  for key_state, query_state in zip(key_cache, query_cache):
273
- # Compute attention weights
274
- attn_weights = get_attn_weights(
275
- key_state[:, :, :query_upper_bound, :],
276
- query_state
277
  )
278
- # Average over query positions
279
- attn_weights = attn_weights.mean(dim=-2)
280
-
281
- # Aggregate scores for each chunk
282
- chunk_scores = []
283
- for start, end in chunk_ranges:
284
- chunk_scores.append(attn_weights[:, :, start:end].sum(dim=-1))
285
- chunk_scores = torch.stack(chunk_scores, dim=2)
286
  all_head_scores.append(chunk_scores)
287
-
288
- # Stack all layers: [batch, num_layers, num_heads, num_chunks]
289
  all_head_scores = torch.stack(all_head_scores, dim=1).float()
290
-
291
  # Select specific QR heads
292
  if qr_head_list is not None:
293
  head_set = [tuple(map(int, h.split('-'))) for h in qr_head_list.split(',')]
294
- indices = torch.tensor(head_set).to(all_head_scores.device)
295
- layers, heads = indices[:, 0], indices[:, 1]
296
- all_head_scores = all_head_scores[:, layers, heads, :]
297
-
298
- # Sum over selected heads
299
- scores = all_head_scores.sum(dim=1).squeeze(0)
300
-
301
- return scores
302
  ```
303
 
304
- ### 3. Complete Inference Pipeline
305
 
306
  ```python
307
  from custom_cache_new import DynamicCacheWithQuery
308
 
309
  def rerank_documents(model, tokenizer, question, paragraphs, qr_head_list, device):
310
  """
311
- Rerank documents based on QRRanker scores.
312
-
313
  Args:
314
- model: QRRanker model
315
- tokenizer: Tokenizer
316
  question: Query string
317
- paragraphs: List of paragraph dicts with 'idx' and 'paragraph_text'
318
- qr_head_list: QR head list string (e.g., "20-15,21-11,17-27,...")
319
  device: torch device
320
-
321
  Returns:
322
- ranked_ids: List of paragraph IDs sorted by relevance
323
- scores: Corresponding relevance scores
324
  """
325
- # Build input sequence
326
- prompt_prefix = '<|im_start|>user\n'
327
- retrieval_instruction = "Here are some retrieved chunks:\n\n"
328
-
329
- chunk_part = prompt_prefix + retrieval_instruction
330
  chunk_ranges = []
331
-
332
  for i, p in enumerate(paragraphs):
333
  text = p.get('title', '') + ': ' + p['paragraph_text']
334
  chunk_part += f"[{i+1}]"
@@ -337,133 +201,110 @@ def rerank_documents(model, tokenizer, question, paragraphs, qr_head_list, devic
337
  end = len(chunk_part)
338
  chunk_ranges.append([start, end])
339
  chunk_part += '\n\n'
340
-
341
  query_part = f"Use the retrieved chunks to answer the user's query.\n\nQuery: {question}"
342
  full_seq = chunk_part + query_part
343
-
344
- # Tokenize with offset mapping
345
- inputs = tokenizer(
346
- full_seq,
347
- max_length=262144,
348
- truncation=True,
349
- return_tensors='pt',
350
- return_offsets_mapping=True,
351
- add_special_tokens=False
352
- )
353
-
354
  input_ids = inputs['input_ids'].to(device)
355
  attention_mask = inputs['attention_mask'].to(device)
356
  offset_mapping = inputs['offset_mapping'][0]
357
-
358
- # Build character-to-token mapping
359
  char_to_token = {}
360
  for i, (s, e) in enumerate(offset_mapping):
361
  for j in range(s, e):
362
  char_to_token[j] = i
363
-
364
- # Map chunk character ranges to token ranges
365
- token_chunk_ranges = []
366
- for start, end in chunk_ranges:
367
- token_start = char_to_token.get(start, 0)
368
- token_end = char_to_token.get(end - 1, 0) + 1
369
- token_chunk_ranges.append([token_start, token_end])
370
-
371
- # Get query token positions
372
- query_start_char = full_seq.index(question)
373
- query_end_char = query_start_char + len(question) - 1
374
  query_positions = list(range(
375
- char_to_token[query_start_char],
376
- char_to_token[query_end_char] + 1
377
  ))
378
  query_upper_bound = query_positions[-1] + 1
379
-
380
- # Forward pass with custom cache
381
  with torch.no_grad():
382
- # Initialize cache with query token positions
383
  past_kv = DynamicCacheWithQuery(query_indices=query_positions)
384
-
385
- # Run model forward pass
386
  output = model(input_ids, attention_mask, past_key_values=past_kv)
387
-
388
- # Extract query and key states from cache
389
- query_cache = output.past_key_values.query_cache
390
- key_cache = output.past_key_values.key_cache
391
-
392
- # Compute relevance scores
393
  scores = compute_qr_scores(
394
- query_cache, key_cache,
 
395
  qr_head_list, token_chunk_ranges, query_upper_bound
396
  )
397
-
398
- # Sort by scores (descending)
399
- sorted_indices = torch.argsort(scores, descending=True).cpu().tolist()
400
- ranked_ids = [paragraphs[i]['idx'] for i in sorted_indices]
401
- ranked_scores = [float(scores[i]) for i in sorted_indices]
402
-
403
- return ranked_ids, ranked_scores
404
- ```
405
-
406
- ## Model Configuration
407
-
408
- The model configuration includes the following QRRanker-specific parameters:
409
-
410
- | Parameter | Description |
411
- |-----------|-------------|
412
- | `qr_start_layer` | Starting layer index for QR heads |
413
- | `qr_end_layer` | Ending layer index for QR heads |
414
- | `qr_head_list` | List of (layer, head) tuples for top QR heads |
415
-
416
- ### Default Top-16 QR Heads
417
 
418
- ```
419
- 20-15, 21-11, 17-27, 23-10, 22-4, 21-10, 21-8, 21-18,
420
- 18-15, 18-19, 17-25, 17-17, 24-13, 17-4, 19-12, 21-31
421
  ```
422
 
423
- ## Command Line Usage
424
 
425
- ```bash
426
- # Basic inference
427
- python qr_ranker_inference.py \
428
- --base_model MindscapeRAG/QRRanker \
429
- --data_path /path/to/data.json \
430
- --mode top16
431
-
432
- # With summary
433
- python qr_ranker_inference.py \
434
- --base_model MindscapeRAG/QRRanker \
435
- --data_path /path/to/data.json \
436
- --mode top16 \
437
- --use_summary
 
 
438
  ```
439
 
440
- ### Arguments
 
 
 
 
 
 
 
 
 
 
441
 
442
- | Argument | Type | Default | Description |
443
- |----------|------|---------|-------------|
444
- | `--base_model` | str | required | Path to QRRanker model |
445
- | `--data_path` | str | required | Path to input data file |
446
- | `--output_dir` | str | `./outputs` | Output directory |
447
- | `--mode` | str | `top16` | Mode: `full` (all heads) or `top16` (selected heads) |
448
- | `--qr_head_list` | str | None | Custom QR head list |
449
- | `--use_summary` | flag | False | Use summary field in data |
450
 
 
 
 
 
 
 
 
 
451
 
 
 
 
 
452
 
453
- If you use our QRRanker, please kindly cite:
454
 
455
  ```bibtex
456
  @misc{li2026queryfocusedmemoryawarererankerlong,
457
- title={Query-focused and Memory-aware Reranker for Long Context Processing},
458
  author={Yuqing Li and Jiangnan Li and Mo Yu and Guoxuan Ding and Zheng Lin and Weiping Wang and Jie Zhou},
459
  year={2026},
460
  eprint={2602.12192},
461
  archivePrefix={arXiv},
462
  primaryClass={cs.CL},
463
- url={https://arxiv.org/abs/2602.12192},
464
  }
465
  ```
466
 
467
  ## License
468
 
469
- This project is licensed under the Apache 2.0 License.
 
30
 
31
  QRRanker is a lightweight reranking framework that leverages **Query-focused Retrieval (QR) heads** to produce continuous relevance scores, enabling effective listwise reranking with small-scale models.
32
 
 
33
  ## Model Description
34
 
35
  Built upon the existing analysis of retrieval heads in large language models, QRRanker trains models to estimate passage–query relevance using the attention scores of selected **Query-focused Retrieval (QR) heads**. These heads are identified through QR score computation on seed data and are particularly effective at capturing query-document relevance signals.
 
41
  - **Listwise Reranking**: Leverages holistic information within the entire candidate shortlist during ranking
42
  - **Continuous Relevance Scores**: Enables training on arbitrary retrieval datasets without requiring Likert-scale supervision
43
  - **Selective Head Usage**: Focuses on top-performing QR attention heads
44
+ - **Layer Truncation**: Only the first 25 of 36 layers are retained — all QR heads fall within layers 17–24, so deeper layers are unnecessary
45
  - **Memory Enhancement**: Optional contextual summaries for improved accuracy on long narratives and dialogues
46
 
47
+ ## Architecture
48
 
49
+ This model is a **layer-truncated** version of Qwen3-4B-Instruct-2507. The original model has 36 transformer layers, but only the first **25 layers** are retained. The top-performing QR heads (layers 17–24) all fall within this range — deeper layers contribute no useful QR signal but consume extra computation and memory.
50
 
51
+ Key design choices in `modeling_qwen3_qr.py`:
52
 
53
+ - **`Qwen3ConfigGating`**: Extends `Qwen3Config` with `qr_start_layer`, `qr_end_layer`, `qr_head_list`, and `qr_head_list_mapped` (head indices remapped relative to `qr_start_layer`)
54
+ - **Layer construction**: Only instantiates `qr_end_layer` (25) layers instead of all `num_hidden_layers` (36)
55
+ - **No final norm**: Skips `self.norm(hidden_states)` since we only need intermediate KV/query caches, not the final hidden state
56
+ - **`DynamicCacheWithQuery`**: Custom KV-cache that additionally stores query states at specified token positions during the forward pass
57
 
58
+ ### Default Top-16 QR Heads
 
 
 
 
 
 
 
 
59
 
 
 
60
  ```
61
+ Layer-Head: 20-15, 21-11, 17-27, 23-10, 22-4, 21-10, 21-8, 21-18,
62
+ 18-15, 18-19, 17-25, 17-17, 24-13, 17-4, 19-12, 21-31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  ```
64
 
65
+ All selected heads fall within layers 17–24, which is why truncation to 25 layers is safe.
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ ### Model Configuration
68
 
69
+ | Parameter | Value | Description |
70
+ |-----------|-------|-------------|
71
+ | `qr_start_layer` | 17 | First layer containing QR heads |
72
+ | `qr_end_layer` | 25 | Layers 0–24 are retained; layers 25–35 are removed |
73
+ | `qr_head_list` | 16 (layer, head) pairs | Top QR heads using original layer indices |
74
+ | `qr_head_list_mapped` | 16 (layer, head) pairs | QR heads with layer indices remapped relative to `qr_start_layer` |
75
+ | `num_hidden_layers` | 36 | Original full model depth (config only, not instantiated) |
76
+ | `num_attention_heads` | 32 | Attention heads per layer |
77
+ | `num_key_value_heads` | 8 | GQA key-value heads per layer |
78
 
79
+ ## Quick Start
80
 
81
+ ### Loading the Model
82
 
83
  ```python
 
 
84
  import torch
85
+ from transformers import AutoModel, AutoConfig, AutoTokenizer
86
 
87
+ # Load model — trust_remote_code loads the layer-truncated Qwen3Model
88
+ # and Qwen3ConfigGating automatically via auto_map in config.json
89
+ config = AutoConfig.from_pretrained("MindscapeRAG/QRRanker", trust_remote_code=True)
90
+ model = AutoModel.from_pretrained(
91
+ "MindscapeRAG/QRRanker",
92
+ config=config,
93
+ torch_dtype=torch.float16,
94
+ trust_remote_code=True,
95
+ ).cuda().eval()
96
 
97
+ tokenizer = AutoTokenizer.from_pretrained("MindscapeRAG/QRRanker")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  ```
99
 
100
+ ### QR Score Computation
101
+
102
+ After a forward pass, QR scores are computed from the cached query and key states:
103
 
104
  ```python
105
  import math
 
106
 
107
+ def repeat_kv(hidden_states, n_rep):
108
+ """Expand KV heads to match query heads (GQA)."""
109
+ batch, num_kv_heads, slen, head_dim = hidden_states.shape
110
  if n_rep == 1:
111
  return hidden_states
112
+ hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_kv_heads, n_rep, slen, head_dim)
113
+ return hidden_states.reshape(batch, num_kv_heads * n_rep, slen, head_dim)
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
 
116
  def get_attn_weights(key_states, query_states):
117
+ """Compute softmax attention weights with causal mask."""
118
  bsz, num_heads, q_len, head_dim = query_states.size()
119
+ num_kv_heads = key_states.size(1)
120
+ key_states = repeat_kv(key_states, num_heads // num_kv_heads)
 
 
 
 
121
 
 
122
  scale = 1.0 / math.sqrt(head_dim)
123
+ attn_weights = torch.matmul(query_states * scale, key_states.transpose(2, 3))
124
+
125
+ # Causal mask
126
+ seq_len = attn_weights.size(-1)
127
+ causal_mask = torch.ones(num_heads, q_len, seq_len, device=attn_weights.device)
128
+ causal_mask = torch.triu(causal_mask.transpose(-1, -2), diagonal=-(seq_len - q_len)).transpose(-1, -2)
129
+ attn_weights += ((1 - causal_mask) * torch.finfo(attn_weights.dtype).min).unsqueeze(0)
130
+
131
  attn_lses = torch.logsumexp(attn_weights, dim=-1, keepdim=True)
132
+ return torch.exp(attn_weights - attn_lses)
 
 
 
133
 
 
134
 
135
+ def compute_qr_scores(query_cache, key_cache, qr_head_list, chunk_ranges, query_upper_bound):
 
 
 
 
 
 
 
136
  """
137
+ Compute QRRanker relevance scores for document chunks.
138
+
139
  Args:
140
+ query_cache: List[Tensor] query states per layer from DynamicCacheWithQuery
141
+ key_cache: List[Tensor] key states per layer
142
+ qr_head_list: str e.g. "20-15,21-11,17-27,..."
143
+ chunk_ranges: List[[start, end]] token ranges for each chunk
144
+ query_upper_bound: int upper bound of query token positions
145
+
146
  Returns:
147
+ scores: Tensor of shape [num_chunks]
148
  """
149
  all_head_scores = []
 
150
  for key_state, query_state in zip(key_cache, query_cache):
151
+ attn_weights = get_attn_weights(key_state[:, :, :query_upper_bound, :], query_state)
152
+ attn_weights = attn_weights.mean(dim=-2) # average over query positions
153
+ chunk_scores = torch.stack(
154
+ [attn_weights[:, :, s:e].sum(dim=-1) for s, e in chunk_ranges], dim=2
155
  )
 
 
 
 
 
 
 
 
156
  all_head_scores.append(chunk_scores)
157
+
158
+ # [batch, num_layers, num_heads, num_chunks]
159
  all_head_scores = torch.stack(all_head_scores, dim=1).float()
160
+
161
  # Select specific QR heads
162
  if qr_head_list is not None:
163
  head_set = [tuple(map(int, h.split('-'))) for h in qr_head_list.split(',')]
164
+ indices = torch.tensor(head_set, device=all_head_scores.device)
165
+ all_head_scores = all_head_scores[:, indices[:, 0], indices[:, 1], :]
166
+
167
+ return all_head_scores.sum(dim=1).squeeze(0)
 
 
 
 
168
  ```
169
 
170
+ ### Complete Inference Pipeline
171
 
172
  ```python
173
  from custom_cache_new import DynamicCacheWithQuery
174
 
175
  def rerank_documents(model, tokenizer, question, paragraphs, qr_head_list, device):
176
  """
177
+ Rerank candidate paragraphs by QRRanker relevance scores.
178
+
179
  Args:
180
+ model: QRRanker model (loaded with trust_remote_code=True)
181
+ tokenizer: Corresponding tokenizer
182
  question: Query string
183
+ paragraphs: List of dicts with 'idx', 'title', 'paragraph_text'
184
+ qr_head_list: str e.g. "20-15,21-11,17-27,..."
185
  device: torch device
186
+
187
  Returns:
188
+ ranked_ids: Paragraph indices sorted by descending relevance
189
+ ranked_scores: Corresponding scores
190
  """
191
+ # Build input: [chunks] + [query]
192
+ prompt_prefix = '<|im_start|>user\nHere are some retrieved chunks:\n\n'
193
+ chunk_part = prompt_prefix
 
 
194
  chunk_ranges = []
195
+
196
  for i, p in enumerate(paragraphs):
197
  text = p.get('title', '') + ': ' + p['paragraph_text']
198
  chunk_part += f"[{i+1}]"
 
201
  end = len(chunk_part)
202
  chunk_ranges.append([start, end])
203
  chunk_part += '\n\n'
204
+
205
  query_part = f"Use the retrieved chunks to answer the user's query.\n\nQuery: {question}"
206
  full_seq = chunk_part + query_part
207
+
208
+ # Tokenize
209
+ inputs = tokenizer(full_seq, max_length=262144, truncation=True,
210
+ return_tensors='pt', return_offsets_mapping=True, add_special_tokens=False)
 
 
 
 
 
 
 
211
  input_ids = inputs['input_ids'].to(device)
212
  attention_mask = inputs['attention_mask'].to(device)
213
  offset_mapping = inputs['offset_mapping'][0]
214
+
215
+ # Character-to-token mapping
216
  char_to_token = {}
217
  for i, (s, e) in enumerate(offset_mapping):
218
  for j in range(s, e):
219
  char_to_token[j] = i
220
+
221
+ token_chunk_ranges = [
222
+ [char_to_token.get(s, 0), char_to_token.get(e - 1, 0) + 1]
223
+ for s, e in chunk_ranges
224
+ ]
225
+
226
+ query_start = full_seq.index(question)
 
 
 
 
227
  query_positions = list(range(
228
+ char_to_token[query_start],
229
+ char_to_token[query_start + len(question) - 1] + 1
230
  ))
231
  query_upper_bound = query_positions[-1] + 1
232
+
233
+ # Forward pass
234
  with torch.no_grad():
 
235
  past_kv = DynamicCacheWithQuery(query_indices=query_positions)
 
 
236
  output = model(input_ids, attention_mask, past_key_values=past_kv)
 
 
 
 
 
 
237
  scores = compute_qr_scores(
238
+ output.past_key_values.query_cache,
239
+ output.past_key_values.key_cache,
240
  qr_head_list, token_chunk_ranges, query_upper_bound
241
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
 
243
+ sorted_idx = torch.argsort(scores, descending=True).cpu().tolist()
244
+ return [paragraphs[i]['idx'] for i in sorted_idx], [float(scores[i]) for i in sorted_idx]
 
245
  ```
246
 
247
+ ## Input Data Format
248
 
249
+ ```json
250
+ {
251
+ "id": "sample_001",
252
+ "question": "What is the capital of France?",
253
+ "answer": "Paris",
254
+ "paragraphs": [
255
+ {
256
+ "idx": 0,
257
+ "title": "France",
258
+ "paragraph_text": "Paris is the capital and largest city of France...",
259
+ "is_supporting": true
260
+ }
261
+ ],
262
+ "summary": "Optional summary text..."
263
+ }
264
  ```
265
 
266
+ | Field | Type | Required | Description |
267
+ |-------|------|----------|-------------|
268
+ | `id` | string | Yes | Unique sample identifier |
269
+ | `question` | string | Yes | User query/question |
270
+ | `answer` | string | No | Ground truth answer (for evaluation) |
271
+ | `paragraphs` | list | Yes | List of candidate paragraphs |
272
+ | `paragraphs[].idx` | int | Yes | Paragraph index |
273
+ | `paragraphs[].title` | string | No | Paragraph title |
274
+ | `paragraphs[].paragraph_text` | string | Yes | Paragraph content |
275
+ | `paragraphs[].is_supporting` | bool | No | Whether it's a supporting paragraph (for evaluation) |
276
+ | `summary` | string | No | Optional summary information |
277
 
278
+ ## Environment
 
 
 
 
 
 
 
279
 
280
+ | Package | Version |
281
+ |---------|---------|
282
+ | Python | 3.10 |
283
+ | torch | 2.7.1 |
284
+ | transformers | 4.53.0 |
285
+ | flash-attn | (required for `flash_attention_2`) |
286
+ | safetensors | 0.5.3 |
287
+ | tokenizers | 0.21.2 |
288
 
289
+ ```bash
290
+ pip install torch==2.7.1 transformers==4.53.0 safetensors
291
+ pip install flash-attn --no-build-isolation
292
+ ```
293
 
294
+ ## Citation
295
 
296
  ```bibtex
297
  @misc{li2026queryfocusedmemoryawarererankerlong,
298
+ title={Query-focused and Memory-aware Reranker for Long Context Processing},
299
  author={Yuqing Li and Jiangnan Li and Mo Yu and Guoxuan Ding and Zheng Lin and Weiping Wang and Jie Zhou},
300
  year={2026},
301
  eprint={2602.12192},
302
  archivePrefix={arXiv},
303
  primaryClass={cs.CL},
304
+ url={https://arxiv.org/abs/2602.12192},
305
  }
306
  ```
307
 
308
  ## License
309
 
310
+ This project is licensed under the Apache 2.0 License.