ShihaoW commited on
Commit
8d21b77
·
verified ·
1 Parent(s): 272068e

Add batch inference and LA Flash runtime

Browse files
README.md CHANGED
@@ -31,7 +31,8 @@ base_model:
31
  * 🚀 **Online Demo**: [LocateAnything (Hugging Face Spaces)](https://huggingface.co/spaces/nvidia/LocateAnything)
32
  * 💻 **GitHub Code**: [NVlabs/Eagle/Embodied](https://github.com/NVlabs/Eagle/tree/main/Embodied)
33
  * 📄 **Paper**: [arXiv:2605.27365](https://arxiv.org/abs/2605.27365)
34
- *
 
35
  # Model Overview
36
 
37
  ### Description:
@@ -263,6 +264,37 @@ Test Hardware: H100
263
 
264
  We suggest using `max_new_tokens=8192` and `generation_mode="hybrid"` to avoid truncated response and balance speed with accuracy.
265
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  ### Installation
267
 
268
  ```bash
@@ -489,9 +521,36 @@ points = LocateAnythingWorker.parse_points(result["answer"], w, h)
489
  | `slow` | Pure auto-regressive decoding | Slowest | Most robust |
490
  | `hybrid` (default) | MTP first, falls back to AR on uncertain boxes, switches back after box boundary | Balanced | Best overall |
491
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
  ## Ethical Considerations:
493
  NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal model team to ensure this model meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
494
 
495
  Please make sure you have proper rights and permissions for all input image and video content; if image or video includes people, personal health information, or intellectual property, the image or video generated will not blur or maintain proportions of image subjects included.
496
 
497
- Please report model quality, risk, security vulnerabilities or NVIDIA AI Concerns [here](https://app.intigriti.com/programs/nvidia/nvidiavdp/detail).
 
31
  * 🚀 **Online Demo**: [LocateAnything (Hugging Face Spaces)](https://huggingface.co/spaces/nvidia/LocateAnything)
32
  * 💻 **GitHub Code**: [NVlabs/Eagle/Embodied](https://github.com/NVlabs/Eagle/tree/main/Embodied)
33
  * 📄 **Paper**: [arXiv:2605.27365](https://arxiv.org/abs/2605.27365)
34
+
35
+
36
  # Model Overview
37
 
38
  ### Description:
 
264
 
265
  We suggest using `max_new_tokens=8192` and `generation_mode="hybrid"` to avoid truncated response and balance speed with accuracy.
266
 
267
+ ### Batch Hybrid Inference
268
+
269
+ This release includes `batch_infer.py`, `batch_utils`, and `kernel_utils` for
270
+ high-throughput detection and grounding. The `la_flash` backend is a pure
271
+ FlashAttention-varlen sparse range executor: it keeps LocateAnything's hybrid
272
+ MTP decoding path, avoids dense `[B,H,Q,K]` SDPA masks, and does not require a
273
+ custom CUDA extension build.
274
+
275
+ Use it with:
276
+
277
+ ```bash
278
+ python batch_infer.py \
279
+ --model . \
280
+ --attn la_flash \
281
+ --scheduler pipeline \
282
+ --batch-size 4 \
283
+ --image /path/to/image.jpg \
284
+ --query "person</c>car"
285
+ ```
286
+
287
+ A100 4K probe, real 3840x2160 street image, `query=vehicle`,
288
+ `batch_size=4`, raw PIL input, `in_token_limit=25600`, hybrid MTP inference:
289
+
290
+ | Backend | Attention Path | Time | Peak Reserved Memory |
291
+ | --- | --- | ---: | ---: |
292
+ | `sdpa` | Dense SDPA masks | 8.2600 s | 35.12 GB |
293
+ | `la_flash` | FlashAttention sparse range plan | 8.0314 s | 11.71 GB |
294
+
295
+ See `batch_utils/README.md` and `kernel_utils/README.md` for runtime knobs and
296
+ implementation details.
297
+
298
  ### Installation
299
 
300
  ```bash
 
521
  | `slow` | Pure auto-regressive decoding | Slowest | Most robust |
522
  | `hybrid` (default) | MTP first, falls back to AR on uncertain boxes, switches back after box boundary | Balanced | Best overall |
523
 
524
+ ## Batch Utils and Kernel Utils
525
+
526
+ This repository also includes optional utilities for high-throughput detection
527
+ runs:
528
+
529
+ - `batch_infer.py`: JSONL/image-query batch inference CLI.
530
+ - `batch_utils/`: batched hybrid generation runtime. See
531
+ `batch_utils/README.md`.
532
+ - `kernel_utils/`: LA Flash sparse range utilities. See
533
+ `kernel_utils/README.md`.
534
+
535
+ Run a small batch inference job:
536
+
537
+ ```bash
538
+ python batch_infer.py \
539
+ --model . \
540
+ --attn la_flash \
541
+ --scheduler pipeline \
542
+ --batch-size 4 \
543
+ --image assets/pointing.png \
544
+ --query "the object being pointed at"
545
+ ```
546
+
547
+ The batched sparse-plan decode runtime is intended for inference/evaluation and
548
+ does not support the training `labels` path. Training remains on the
549
+ MagiAttention backend.
550
+
551
  ## Ethical Considerations:
552
  NVIDIA believes Trustworthy AI is a shared responsibility and we have established policies and practices to enable development for a wide array of AI applications. When downloaded or used in accordance with our terms of service, developers should work with their internal model team to ensure this model meets requirements for the relevant industry and use case and addresses unforeseen product misuse.
553
 
554
  Please make sure you have proper rights and permissions for all input image and video content; if image or video includes people, personal health information, or intellectual property, the image or video generated will not blur or maintain proportions of image subjects included.
555
 
556
+ Please report model quality, risk, security vulnerabilities or NVIDIA AI Concerns [here](https://app.intigriti.com/programs/nvidia/nvidiavdp/detail).
batch_infer.py ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Minimal batch inference CLI for the LocateAnything-3B release code.
3
+
4
+ Examples:
5
+ python batch_infer.py --model /path/to/LocateAnything-3B --attn sdpa \
6
+ --image demo.jpg --query "person</c>car"
7
+
8
+ python batch_infer.py --requests requests.jsonl --batch-size 16 --attn la_flash
9
+
10
+ Each JSONL request should contain {"image": "/path/to.jpg", "query": "person</c>car"}.
11
+ """
12
+ import argparse
13
+ import json
14
+ import os
15
+ from pathlib import Path
16
+
17
+ from PIL import Image
18
+
19
+
20
+ def _attn_arg(value):
21
+ mode = (value or "sdpa").strip().lower().replace("-", "_")
22
+ aliases = {
23
+ "": "sdpa",
24
+ "manual": "eager",
25
+ "torch": "eager",
26
+ "torch_eager": "eager",
27
+ "torch_sdpa": "sdpa",
28
+ "flash": "la_flash",
29
+ "la_flash": "la_flash",
30
+ "kernel": "la_flash",
31
+ "cuda": "la_flash",
32
+ "range": "la_flash",
33
+ "range_attention": "la_flash",
34
+ }
35
+ mode = aliases.get(mode, mode)
36
+ if mode not in {"sdpa", "eager", "magi", "la_flash"}:
37
+ raise argparse.ArgumentTypeError(
38
+ f"--attn must be one of sdpa, eager, magi, la_flash; got {value!r}"
39
+ )
40
+ return mode
41
+
42
+
43
+ def _load_requests(args):
44
+ requests = []
45
+ if args.requests:
46
+ with open(args.requests, "r", encoding="utf-8") as f:
47
+ for line in f:
48
+ if not line.strip():
49
+ continue
50
+ row = json.loads(line)
51
+ requests.append((row["image"], row["query"]))
52
+ if args.image or args.query:
53
+ if len(args.image or []) != len(args.query or []):
54
+ raise ValueError("--image and --query must appear the same number of times")
55
+ requests.extend(zip(args.image, args.query))
56
+ if not requests:
57
+ raise ValueError("provide --requests JSONL or at least one --image/--query pair")
58
+ return requests
59
+
60
+
61
+ def main():
62
+ ap = argparse.ArgumentParser()
63
+ ap.add_argument("--requests", help="JSONL file with image/query fields")
64
+ ap.add_argument("--image", action="append", help="Image path; repeat with --query")
65
+ ap.add_argument("--query", action="append", help="Category query, e.g. person</c>car")
66
+ ap.add_argument("--model", default=os.environ.get("LA_FLASH_MODEL", "nvidia/LocateAnything-3B"))
67
+ ap.add_argument("--attn", type=_attn_arg, default=os.environ.get("LA_FLASH_ATTN", "sdpa"),
68
+ help="LLM attention backend: sdpa, eager, magi, or la_flash")
69
+ ap.add_argument("--vision-attn", default=os.environ.get("LA_FLASH_VISION_ATTN", "auto"),
70
+ choices=["auto", "flash_attention_2", "sdpa", "eager"])
71
+ ap.add_argument("--batch-size", type=int, default=1)
72
+ ap.add_argument("--scheduler", default=os.environ.get("LA_FLASH_HYBRID_SCHEDULER", "eager"),
73
+ choices=["eager", "hold_ar", "ar_first", "pipeline", "adaptive"])
74
+ ap.add_argument("--group-size", type=int, default=int(os.environ.get("LA_FLASH_HYBRID_GROUP_SIZE", "0")))
75
+ ap.add_argument("--max-new-tokens", type=int, default=2048)
76
+ ap.add_argument("--temperature", type=float, default=0.7)
77
+ ap.add_argument("--top-p", type=float, default=0.9)
78
+ ap.add_argument("--top-k", type=int, default=0)
79
+ ap.add_argument("--repetition-penalty", type=float, default=1.1)
80
+ ap.add_argument("--strict-attn", action="store_true",
81
+ help="Fail instead of falling back to SDPA if magi/la_flash is unavailable")
82
+ ap.add_argument("--out", default="", help="Optional output JSONL path; stdout if omitted")
83
+ args = ap.parse_args()
84
+ args.attn = _attn_arg(args.attn)
85
+
86
+ os.environ["LA_FLASH_MODEL"] = args.model
87
+ os.environ["LA_FLASH_ATTN"] = args.attn
88
+ os.environ["LA_FLASH_VISION_ATTN"] = args.vision_attn
89
+ os.environ["LA_FLASH_HYBRID_SCHEDULER"] = args.scheduler
90
+ os.environ["LA_FLASH_HYBRID_GROUP_SIZE"] = str(args.group_size)
91
+ if args.strict_attn:
92
+ os.environ["LA_FLASH_STRICT_ATTN"] = "1"
93
+
94
+ from batch_utils import generate_batch_hybrid, get_last_hybrid_stats, load
95
+ from batch_utils.hybrid_runtime import load_pil
96
+
97
+ requests = _load_requests(args)
98
+ load()
99
+
100
+ writer = open(args.out, "w", encoding="utf-8") if args.out else None
101
+ try:
102
+ for start in range(0, len(requests), max(1, args.batch_size)):
103
+ chunk = requests[start:start + max(1, args.batch_size)]
104
+ pairs = [(load_pil(image), query) for image, query in chunk]
105
+ texts = generate_batch_hybrid(
106
+ pairs,
107
+ temperature=args.temperature,
108
+ top_p=None if args.top_p < 0 else args.top_p,
109
+ top_k=None if args.top_k <= 0 else args.top_k,
110
+ repetition_penalty=args.repetition_penalty,
111
+ max_new_tokens=args.max_new_tokens,
112
+ scheduler=args.scheduler,
113
+ group_size=args.group_size,
114
+ )
115
+ stats = get_last_hybrid_stats()
116
+ for (image, query), text in zip(chunk, texts):
117
+ row = {"image": str(Path(image)), "query": query, "raw_response": text, "stats": stats}
118
+ line = json.dumps(row, ensure_ascii=False)
119
+ if writer:
120
+ writer.write(line + "\n")
121
+ else:
122
+ print(line, flush=True)
123
+ finally:
124
+ if writer:
125
+ writer.close()
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
batch_utils/README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Batch Utils
2
+
3
+ `batch_utils` contains the optional batched hybrid generation runtime for
4
+ LocateAnything. It keeps the model loading, tokenization, image feature caching,
5
+ sampling, and scheduler code used by `batch_infer.py` and the detection
6
+ experiments.
7
+
8
+ ## Runtime Modes
9
+
10
+ - `LA_FLASH_ATTN=sdpa`: stock PyTorch SDPA path.
11
+ - `LA_FLASH_ATTN=eager`: eager attention path for debugging.
12
+ - `LA_FLASH_ATTN=magi`: MagiAttention path when MagiAttention is installed.
13
+ - `LA_FLASH_ATTN=la_flash`: LA Flash sparse range backend
14
+ from `kernel_utils`.
15
+
16
+ ## Common Knobs
17
+
18
+ | Variable | Default | Meaning |
19
+ | --- | --- | --- |
20
+ | `LA_FLASH_MODEL` | `nvidia/LocateAnything-3B` | HF model id or local model directory. |
21
+ | `LA_FLASH_ATTN` | `sdpa` | LLM attention backend. |
22
+ | `LA_FLASH_VISION_ATTN` | `auto` | Vision encoder attention: `auto`, `flash_attention_2`, `sdpa`, or `eager`. |
23
+ | `LA_FLASH_STRICT_ATTN` | `0` | Set `1` to fail instead of falling back to SDPA. |
24
+ | `LA_FLASH_HYBRID_SCHEDULER` | `eager` | Hybrid decode scheduler. |
25
+ | `LA_FLASH_HYBRID_GROUP_SIZE` | `0` | Scheduler group size; `0` lets the runtime decide. |
26
+ | `LA_FLASH_VISION_ENCODE_BATCH_SIZE` | `8` | Maximum images per MoonViT encode micro-batch. |
27
+ | `LA_FLASH_KV_PACK_TOKEN_BUDGET` | `0` | Optional KV packing memory cap for long-tail batches. |
28
+ | `LA_FLASH_DENSE_BACKEND` | `sdpa` | Dense worker/prefill attention backend. Keep this as `sdpa`; LA Flash is used for sparse range plans. |
29
+ | `LA_FLASH_SEGMENT_FASTPATH` | `auto` | Sparse MTP decode uses FlashAttention varlen multi-segment merge by default. |
30
+
31
+ ## CLI Example
32
+
33
+ ```bash
34
+ python batch_infer.py \
35
+ --model nvidia/LocateAnything-3B \
36
+ --attn la_flash \
37
+ --scheduler pipeline \
38
+ --batch-size 4 \
39
+ --image /path/to/image.jpg \
40
+ --query "person</c>car"
41
+ ```
42
+
43
+ For JSONL input, each row should contain:
44
+
45
+ ```json
46
+ {"image": "/path/to/image.jpg", "query": "person</c>car"}
47
+ ```
48
+
49
+ ## Training Boundary
50
+
51
+ This package is for inference and evaluation. Training remains on the
52
+ MagiAttention backend; the batched sparse-plan decode runtime does not support
53
+ the `labels` training path.
batch_utils/__init__.py ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Hybrid batched inference helpers for NVIDIA LocateAnything-3B."""
2
+ from .hybrid_runtime import load, load_pil
3
+ from .engine_hybrid import generate_batch_hybrid, generate_batch_grouped_hybrid, get_last_hybrid_stats
4
+
5
+ __all__ = [
6
+ "load",
7
+ "load_pil",
8
+ "generate_batch_hybrid",
9
+ "generate_batch_grouped_hybrid",
10
+ "get_last_hybrid_stats",
11
+ ]
12
+ __version__ = "0.1.0"
batch_utils/engine_hybrid.py ADDED
@@ -0,0 +1,1357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Batched hybrid-mode generation for LocateAnything-3B.
2
+
3
+ This module keeps the stock hybrid state machine:
4
+
5
+ MTP -> error_box -> AR
6
+ AR -> box_end_ar -> MTP
7
+
8
+ Rows in a batch may be in different modes. The decode loop therefore stores
9
+ per-row KV caches, packs rows with the same mode for one forward call, then
10
+ unpacks the clean KV back per row.
11
+ """
12
+ import copy
13
+ import importlib
14
+ import os
15
+
16
+ import torch
17
+
18
+
19
+ from .hybrid_runtime import (
20
+ ATTN_MODE,
21
+ AR_BATCH_SAN,
22
+ BATCH_SAN,
23
+ DEV,
24
+ N_FUTURE,
25
+ _encode_images,
26
+ _helpers,
27
+ _pad_generated,
28
+ _set_llm_mode,
29
+ _tokenize,
30
+ _tokenize_cached_image,
31
+ build_magi_scheduler_ranges,
32
+ language_model_forward,
33
+ load,
34
+ sample_next_tokens_batched,
35
+ sample_tokens_batched,
36
+ )
37
+
38
+
39
+ README_MAX_NEW_TOKENS = 2048
40
+ README_TEMPERATURE = 0.7
41
+ README_TOP_P = 0.9
42
+ README_REPETITION_PENALTY = 1.1
43
+
44
+ _LAST_HYBRID_STATS = None
45
+
46
+
47
+ def _row_len(kv):
48
+ return kv[0][0].shape[2]
49
+
50
+
51
+ def _pack_stock_kv_rows(kv_rows, rows, dev):
52
+ """Left-pad per-row real-token KV caches for stock-style decoding."""
53
+ lengths = [0 if kv_rows[r] is None else _row_len(kv_rows[r]) for r in rows]
54
+ kmax = max(lengths) if lengths else 0
55
+ if kmax == 0:
56
+ return None, torch.zeros((len(rows), 0), dtype=torch.long, device=dev), lengths, 0
57
+
58
+ ref = next(kv_rows[r] for r in rows if kv_rows[r] is not None)
59
+ packed = []
60
+ for layer in range(len(ref)):
61
+ ref_k, ref_v = ref[layer]
62
+ ks, vs = [], []
63
+ for r, length in zip(rows, lengths):
64
+ if length == 0:
65
+ k = ref_k.new_zeros((1, ref_k.shape[1], kmax, ref_k.shape[3]))
66
+ v = ref_v.new_zeros((1, ref_v.shape[1], kmax, ref_v.shape[3]))
67
+ else:
68
+ k, v = kv_rows[r][layer]
69
+ if length < kmax:
70
+ pad_shape = (1, k.shape[1], kmax - length, k.shape[3])
71
+ k = torch.cat([k.new_zeros(pad_shape), k], dim=2)
72
+ v = torch.cat([v.new_zeros(pad_shape), v], dim=2)
73
+ ks.append(k)
74
+ vs.append(v)
75
+ packed.append((torch.cat(ks, dim=0), torch.cat(vs, dim=0)))
76
+
77
+ kvalid = torch.zeros((len(rows), kmax), dtype=torch.long, device=dev)
78
+ for i, length in enumerate(lengths):
79
+ if length:
80
+ kvalid[i, kmax - length :] = 1
81
+ return tuple(packed), kvalid, lengths, kmax
82
+
83
+
84
+ def _unpack_stock_after_forward(out_kv, local_row, old_len, uncached_len, kmax, umax):
85
+ """Keep old real KV plus the right-aligned uncached real tokens; drop pads/window."""
86
+ out = []
87
+ u0 = kmax + (umax - uncached_len)
88
+ u1 = kmax + umax
89
+ for k, v in out_kv:
90
+ parts_k, parts_v = [], []
91
+ if old_len:
92
+ parts_k.append(k[local_row : local_row + 1, :, kmax - old_len : kmax, :])
93
+ parts_v.append(v[local_row : local_row + 1, :, kmax - old_len : kmax, :])
94
+ if uncached_len:
95
+ parts_k.append(k[local_row : local_row + 1, :, u0:u1, :])
96
+ parts_v.append(v[local_row : local_row + 1, :, u0:u1, :])
97
+ out.append((torch.cat(parts_k, dim=2).contiguous(),
98
+ torch.cat(parts_v, dim=2).contiguous()))
99
+ return tuple(out)
100
+
101
+
102
+ def _mk_generate_kwargs(temperature, top_p, top_k, repetition_penalty, row_temp=None):
103
+ t = temperature if row_temp is None else row_temp
104
+ gk = {"repetition_penalty": repetition_penalty, "generation_mode": "hybrid"}
105
+ if t and t > 0:
106
+ gk["temperature"] = t
107
+ if top_p is not None:
108
+ gk["top_p"] = top_p
109
+ if top_k is not None:
110
+ gk["top_k"] = top_k
111
+ return gk
112
+
113
+
114
+ def _classify_ar_token(token_val, tids):
115
+ if token_val == tids["box_end_token_id"]:
116
+ return "box_end_ar"
117
+ if tids["coord_start_token_id"] <= token_val <= tids["coord_end_token_id"]:
118
+ return "coord_ar"
119
+ if token_val == tids["none_token_id"]:
120
+ return "coord_ar"
121
+ return "im_end"
122
+
123
+
124
+ def _env_flag(name, default=False):
125
+ val = os.environ.get(name)
126
+ if val is None:
127
+ return default
128
+ return val.lower() not in {"0", "false", "no", "off", ""}
129
+
130
+
131
+ def _env_int(name, default):
132
+ val = os.environ.get(name)
133
+ if val is None or val == "":
134
+ return default
135
+ return int(val)
136
+
137
+
138
+ def _kv_pack_token_budget():
139
+ return max(0, _env_int("LA_FLASH_KV_PACK_TOKEN_BUDGET", 0))
140
+
141
+
142
+ def _debug_enabled(debug):
143
+ return _env_flag("LA_FLASH_DEBUG", False) if debug is None else bool(debug)
144
+
145
+
146
+ def _new_hybrid_stats(total_rows, scheduler, group_size, hold_max_steps, adaptive_hold_mtp_max=0):
147
+ return {
148
+ "scheduler": scheduler,
149
+ "requested_group_size": int(group_size or 0),
150
+ "hold_max_steps": int(hold_max_steps),
151
+ "adaptive_hold_mtp_max": int(adaptive_hold_mtp_max),
152
+ "input_batches": 1,
153
+ "input_rows": int(total_rows),
154
+ "groups": 0,
155
+ "group_sizes": [],
156
+ "decode_loops": 0,
157
+ "mixed_mode_cycles": 0,
158
+ "eager_mtp_then_ar_cycles": 0,
159
+ "ar_first_cycles": 0,
160
+ "pipeline_ar_after_mtp_cycles": 0,
161
+ "adaptive_hold_cycles": 0,
162
+ "adaptive_ar_first_cycles": 0,
163
+ "hold_ar_steps": 0,
164
+ "hold_ar_held_mtp_rows": 0,
165
+ "hold_ar_limit_mtp_forwards": 0,
166
+ "mtp_forwards": 0,
167
+ "ar_forwards": 0,
168
+ "mtp_forward_rows": 0,
169
+ "ar_forward_rows": 0,
170
+ "mtp_forward_query_tokens": 0,
171
+ "ar_forward_query_tokens": 0,
172
+ "max_mtp_forward_rows": 0,
173
+ "max_ar_forward_rows": 0,
174
+ "mtp_max_uncached_len": 0,
175
+ "ar_max_uncached_len": 0,
176
+ "mtp_forward_row_hist": {},
177
+ "ar_forward_row_hist": {},
178
+ "prompt_prefill_mode": _hybrid_prefill_mode(),
179
+ "prompt_prefill_forwards": 0,
180
+ "prompt_prefill_forward_rows": 0,
181
+ "prompt_prefill_forward_query_tokens": 0,
182
+ "prompt_prefill_real_tokens": 0,
183
+ "prompt_prefill_shared_groups": 0,
184
+ "prompt_prefill_shared_rows": 0,
185
+ "prompt_prefill_shared_saved_tokens": 0,
186
+ "kv_bucket_splits": 0,
187
+ "kv_bucket_groups": 0,
188
+ "kv_bucket_max_packed_tokens": 0,
189
+ }
190
+
191
+
192
+ def _set_last_hybrid_stats(stats):
193
+ global _LAST_HYBRID_STATS
194
+ _LAST_HYBRID_STATS = copy.deepcopy(stats) if stats is not None else None
195
+
196
+
197
+ def get_last_hybrid_stats():
198
+ """Return scheduler/forward statistics from the most recent hybrid batch."""
199
+ return copy.deepcopy(_LAST_HYBRID_STATS)
200
+
201
+
202
+ def _record_group_stats(stats, bsz):
203
+ if stats is None:
204
+ return
205
+ stats["groups"] += 1
206
+ stats["group_sizes"].append(int(bsz))
207
+
208
+
209
+ def _bump_hist(hist, val):
210
+ key = str(int(val))
211
+ hist[key] = int(hist.get(key, 0)) + 1
212
+
213
+
214
+ def _record_forward_stats(stats, kind, rows, q_len, uncached_lens):
215
+ if stats is None:
216
+ return
217
+ prefix = "mtp" if kind == "mtp" else "ar"
218
+ nrows = int(len(rows))
219
+ q_len = int(q_len)
220
+ stats[f"{prefix}_forwards"] += 1
221
+ stats[f"{prefix}_forward_rows"] += nrows
222
+ stats[f"{prefix}_forward_query_tokens"] += nrows * q_len
223
+ stats[f"max_{prefix}_forward_rows"] = max(stats[f"max_{prefix}_forward_rows"], nrows)
224
+ stats[f"{prefix}_max_uncached_len"] = max(
225
+ stats[f"{prefix}_max_uncached_len"],
226
+ max((int(x) for x in uncached_lens), default=0),
227
+ )
228
+ _bump_hist(stats[f"{prefix}_forward_row_hist"], nrows)
229
+
230
+
231
+ def _record_prefill_stats(stats, rows, q_len, real_tokens, shared_groups=0, shared_rows=0, saved_tokens=0):
232
+ if stats is None:
233
+ return
234
+ nrows = int(rows)
235
+ stats["prompt_prefill_forwards"] += 1
236
+ stats["prompt_prefill_forward_rows"] += nrows
237
+ stats["prompt_prefill_forward_query_tokens"] += nrows * int(q_len)
238
+ stats["prompt_prefill_real_tokens"] += int(real_tokens)
239
+ stats["prompt_prefill_shared_groups"] += int(shared_groups)
240
+ stats["prompt_prefill_shared_rows"] += int(shared_rows)
241
+ stats["prompt_prefill_shared_saved_tokens"] += int(saved_tokens)
242
+
243
+
244
+ def _split_rows_by_kv_budget(rows, kv_rows):
245
+ """Keep dense left-padded KV packs bounded when a few rows become long tails."""
246
+ budget = _kv_pack_token_budget()
247
+ if budget <= 0 or len(rows) <= 1:
248
+ return [rows]
249
+ lengths = [0 if kv_rows[r] is None else _row_len(kv_rows[r]) for r in rows]
250
+ if not lengths or max(lengths) * len(rows) <= budget:
251
+ return [rows]
252
+
253
+ groups = []
254
+ current = []
255
+ current_max = 0
256
+ for row, length in sorted(zip(rows, lengths), key=lambda item: item[1]):
257
+ next_max = max(current_max, int(length))
258
+ if current and next_max * (len(current) + 1) > budget:
259
+ groups.append(current)
260
+ current = [row]
261
+ current_max = int(length)
262
+ else:
263
+ current.append(row)
264
+ current_max = next_max
265
+ if current:
266
+ groups.append(current)
267
+ return groups or [rows]
268
+
269
+
270
+ def _record_kv_bucket_stats(stats, groups, kv_rows):
271
+ if stats is None:
272
+ return
273
+ max_packed = 0
274
+ for group in groups:
275
+ if not group:
276
+ continue
277
+ kmax = max((0 if kv_rows[r] is None else _row_len(kv_rows[r])) for r in group)
278
+ max_packed = max(max_packed, int(kmax) * len(group))
279
+ stats["kv_bucket_max_packed_tokens"] = max(stats["kv_bucket_max_packed_tokens"], max_packed)
280
+ if len(groups) > 1:
281
+ stats["kv_bucket_splits"] += 1
282
+ stats["kv_bucket_groups"] += len(groups)
283
+
284
+
285
+ def _hybrid_scheduler(scheduler):
286
+ val = os.environ.get("LA_FLASH_HYBRID_SCHEDULER", "eager") if scheduler is None else scheduler
287
+ val = str(val).strip().lower()
288
+ aliases = {
289
+ "": "eager",
290
+ "default": "eager",
291
+ "normal": "eager",
292
+ "hold": "hold_ar",
293
+ "hold-ar": "hold_ar",
294
+ "hold_mtp": "hold_ar",
295
+ "hold-mtp": "hold_ar",
296
+ "repair_first": "ar_first",
297
+ "repair-first": "ar_first",
298
+ "ar-first": "ar_first",
299
+ }
300
+ val = aliases.get(val, val)
301
+ if val not in {"eager", "hold_ar", "ar_first", "pipeline", "adaptive"}:
302
+ raise ValueError("scheduler must be one of: eager, hold_ar, ar_first, pipeline, adaptive")
303
+ return val
304
+
305
+
306
+ def _hybrid_group_size(group_size):
307
+ if group_size is None:
308
+ return max(0, _env_int("LA_FLASH_HYBRID_GROUP_SIZE", 0))
309
+ return max(0, int(group_size))
310
+
311
+
312
+ def _hybrid_prefill_mode():
313
+ val = os.environ.get("LA_FLASH_HYBRID_PREFILL", "shared").strip().lower()
314
+ aliases = {
315
+ "0": "none",
316
+ "false": "none",
317
+ "off": "none",
318
+ "legacy": "none",
319
+ "1": "per_row",
320
+ "true": "per_row",
321
+ "on": "per_row",
322
+ "single": "per_row",
323
+ "row": "per_row",
324
+ "rows": "per_row",
325
+ "batched": "batch",
326
+ "prefix": "shared",
327
+ "shared_prefix": "shared",
328
+ "shared-image": "shared",
329
+ "shared_image": "shared",
330
+ "vision": "shared",
331
+ }
332
+ val = aliases.get(val, val)
333
+ if val not in {"none", "per_row", "batch", "shared"}:
334
+ raise ValueError("LA_FLASH_HYBRID_PREFILL must be one of none, per_row, batch, shared")
335
+ return val
336
+
337
+
338
+ def _tolist(t):
339
+ return t.detach().cpu().tolist()
340
+
341
+
342
+ def _safe_decode_rows(tok, input_ids):
343
+ rows = []
344
+ for row in _tolist(input_ids):
345
+ try:
346
+ rows.append(tok.decode(torch.tensor(row), skip_special_tokens=False))
347
+ except Exception:
348
+ rows.append("<decode failed>")
349
+ return rows
350
+
351
+
352
+ def _safe_decode_row(tok, row):
353
+ try:
354
+ return tok.decode(torch.tensor(row), skip_special_tokens=False)
355
+ except Exception:
356
+ return "<decode failed>"
357
+
358
+
359
+ def _effective_allowed_mask(mask2d, q_len, past_len, mtp_window=False):
360
+ """Readable 1/0 q-by-k mask derived from the 2D key-valid mask.
361
+
362
+ This mirrors the model path at a high level:
363
+ causal + padding columns, then the MTP window update
364
+ attn[-block:, -block:] = visible and attn[-block:, -block-1] = masked.
365
+ """
366
+ rows = []
367
+ key_valid = mask2d.detach().cpu().bool()
368
+ total_len = int(key_valid.numel())
369
+ for qi in range(q_len):
370
+ q_abs = past_len + qi
371
+ row = []
372
+ for ki in range(total_len):
373
+ row.append(1 if bool(key_valid[ki]) and ki <= q_abs else 0)
374
+ rows.append(row)
375
+
376
+ if mtp_window and q_len >= N_FUTURE and total_len >= N_FUTURE:
377
+ q0 = q_len - N_FUTURE
378
+ k0 = total_len - N_FUTURE
379
+ for qi in range(q0, q_len):
380
+ for ki in range(k0, total_len):
381
+ rows[qi][ki] = 1
382
+ if k0 - 1 >= 0:
383
+ rows[qi][k0 - 1] = 0
384
+ return rows
385
+
386
+
387
+ def _tail_matrix(mat, rows=None, cols=None):
388
+ if rows is not None:
389
+ mat = mat[-rows:]
390
+ if cols is not None:
391
+ mat = [row[-cols:] for row in mat]
392
+ return mat
393
+
394
+
395
+ def _format_01_matrix(mat):
396
+ return "\n".join(" " + " ".join(str(int(v)) for v in row) for row in mat)
397
+
398
+
399
+ def _safe_sdpa_mask_enabled():
400
+ return _env_flag("LA_FLASH_SDPA_SAFE_4D_MASK", True)
401
+
402
+
403
+ def _build_safe_sdpa_visible_mask(attention_mask_2d, input_ids, past_len, mtp_window=False):
404
+ """Build a 4D 1/0 visible mask, with harmless visibility for all-masked pad queries.
405
+
406
+ The remote Qwen2 SDPA path uses a 2D key-valid mask and can create fully
407
+ masked query rows for left-padded, no-cache prefill. Those rows can produce
408
+ NaNs inside SDPA and later contaminate real tokens through masked K columns.
409
+ This 4D mask keeps real-token visibility identical, and only gives otherwise
410
+ all-masked query rows one valid fallback key so their activations stay finite.
411
+ """
412
+ bsz, q_len = int(input_ids.shape[0]), int(input_ids.shape[1])
413
+ key_len = int(attention_mask_2d.shape[1])
414
+ dev = input_ids.device
415
+ key_valid = attention_mask_2d.to(dtype=torch.bool, device=dev)
416
+ key_idx = torch.arange(key_len, device=dev).view(1, 1, key_len)
417
+ q_abs = (past_len + torch.arange(q_len, device=dev)).view(1, q_len, 1)
418
+ visible = key_valid[:, None, :] & (key_idx <= q_abs)
419
+
420
+ if mtp_window and q_len >= N_FUTURE and key_len >= N_FUTURE:
421
+ k0 = key_len - N_FUTURE
422
+ visible[:, -N_FUTURE:, k0:key_len] = key_valid[:, None, k0:key_len]
423
+ blocked_k = k0 - 1
424
+ if blocked_k >= 0:
425
+ visible[:, -N_FUTURE:, blocked_k] = False
426
+
427
+ row_has_key = visible.any(dim=-1)
428
+ fallback_rows = int((~row_has_key).sum().item())
429
+ if fallback_rows:
430
+ for b in range(bsz):
431
+ valid = torch.nonzero(key_valid[b], as_tuple=False).flatten()
432
+ fallback = int(valid[0].item()) if valid.numel() else 0
433
+ missing = torch.nonzero(~row_has_key[b], as_tuple=False).flatten()
434
+ if missing.numel():
435
+ visible[b, missing, fallback] = True
436
+
437
+ mask = visible[:, None, :, :].to(dtype=torch.bfloat16)
438
+ try:
439
+ mask._la_flash_visible_mask = True
440
+ except Exception:
441
+ pass
442
+ return mask, fallback_rows
443
+
444
+
445
+ def _mask_desc(mask):
446
+ if mask is None:
447
+ return "none"
448
+ if isinstance(mask, dict):
449
+ return "magi_ranges"
450
+ if hasattr(mask, "dim"):
451
+ return "4d_safe_sdpa" if mask.dim() == 4 else "2d_key_valid"
452
+ return type(mask).__name__
453
+
454
+
455
+ def _forward_attention_mask(model, input_ids, attention_mask_2d, past_len, mtp_window=False, range_plan=False):
456
+ llm = model.language_model.model
457
+ if getattr(model, "_la_flash_requested_attn", ATTN_MODE) in {"magi", "la_flash"}:
458
+ range_plan = build_magi_scheduler_ranges(
459
+ model, attention_mask_2d, input_ids, past_len, mtp_window=mtp_window)
460
+ if range_plan is not None:
461
+ return range_plan, 0
462
+ needs_safe_pad = (
463
+ past_len == 0
464
+ and attention_mask_2d is not None
465
+ and attention_mask_2d.dim() == 2
466
+ and input_ids.shape[0] > 1
467
+ )
468
+ if (
469
+ getattr(llm, "_attn_implementation", None) == "sdpa"
470
+ and _safe_sdpa_mask_enabled()
471
+ and needs_safe_pad
472
+ and attention_mask_2d is not None
473
+ and attention_mask_2d.dim() == 2
474
+ ):
475
+ return _build_safe_sdpa_visible_mask(attention_mask_2d, input_ids, past_len, mtp_window)
476
+ return attention_mask_2d, 0
477
+
478
+
479
+ def _actual_sdpa_allowed_masks(model, input_ids, attention_mask, past_len):
480
+ """Recreate the remote Qwen2 SDPA 4D additive mask and return a 0/1 view."""
481
+ llm = model.language_model.model
482
+ mod = importlib.import_module(type(llm).__module__)
483
+ bsz, q_len = int(input_ids.shape[0]), int(input_ids.shape[1])
484
+ dummy = torch.empty(
485
+ (bsz, q_len, 1),
486
+ dtype=torch.bfloat16,
487
+ device=input_ids.device,
488
+ )
489
+ mask4 = mod._prepare_4d_causal_attention_mask(
490
+ attention_mask,
491
+ (bsz, q_len),
492
+ dummy,
493
+ past_len,
494
+ sliding_window=getattr(llm.config, "sliding_window", None),
495
+ )
496
+ remote_ar_decode = q_len == 1 or (
497
+ input_ids is not None and int(input_ids[0, -1].item()) != int(llm.text_mask_token_id)
498
+ )
499
+ if not remote_ar_decode and mask4 is not None and mask4.dim() == 4:
500
+ rows = []
501
+ for b in range(bsz):
502
+ rows.append(
503
+ mod.update_causal_mask_for_one_gen_window_2d(
504
+ input_ids[b],
505
+ mask4[b][0].clone(),
506
+ block_size=int(llm.block_size),
507
+ use_cache=True,
508
+ causal_attn=bool(getattr(llm, "causal_attn", False)),
509
+ ).unsqueeze(0)
510
+ )
511
+ mask4 = torch.stack(rows, dim=0)
512
+ allowed = (mask4[:, 0] >= 0).to(torch.int8).detach().cpu().tolist()
513
+ return allowed, tuple(mask4.shape), remote_ar_decode
514
+
515
+
516
+ def _debug_magi_ranges(q_len, past_len, mtp_window=False):
517
+ kv_len = past_len + q_len
518
+ ar_decode = not mtp_window
519
+ if ar_decode:
520
+ return {
521
+ "q_ranges": [[0, q_len]],
522
+ "k_ranges": [[0, kv_len]],
523
+ "attn_type_map": ["CAUSAL"],
524
+ }
525
+
526
+ block = N_FUTURE
527
+ if not (0 < block <= q_len <= kv_len):
528
+ return {"error": f"invalid magi MTP shape: block={block}, q_len={q_len}, kv_len={kv_len}"}
529
+
530
+ prefix_len = kv_len - block
531
+ blocked_k = prefix_len - 1
532
+ q_ranges, k_ranges, attn_types = [], [], []
533
+ if q_len == kv_len:
534
+ if prefix_len > 0:
535
+ q_ranges.append([0, prefix_len])
536
+ k_ranges.append([0, prefix_len])
537
+ attn_types.append("CAUSAL")
538
+ if prefix_len > 0 and blocked_k > 0:
539
+ q_ranges.append([prefix_len, kv_len])
540
+ k_ranges.append([0, blocked_k])
541
+ attn_types.append("FULL")
542
+ q_ranges.append([prefix_len, kv_len])
543
+ k_ranges.append([prefix_len, kv_len])
544
+ attn_types.append("FULL")
545
+ else:
546
+ recompute = q_len - block
547
+ q_global_start = kv_len - q_len
548
+ for i in range(recompute):
549
+ g = q_global_start + i
550
+ q_ranges.append([i, i + 1])
551
+ k_ranges.append([0, g + 1])
552
+ attn_types.append("FULL")
553
+ q_win = [recompute, q_len]
554
+ if blocked_k > 0:
555
+ q_ranges.append(q_win)
556
+ k_ranges.append([0, blocked_k])
557
+ attn_types.append("FULL")
558
+ q_ranges.append(q_win)
559
+ k_ranges.append([prefix_len, kv_len])
560
+ attn_types.append("FULL")
561
+
562
+ return {"q_ranges": q_ranges, "k_ranges": k_ranges, "attn_type_map": attn_types}
563
+
564
+
565
+ def _print_debug_forward(label, model, tok, input_ids, attention_mask, position_ids,
566
+ past_len, mtp_window=False, extra=None, attention_impl="sdpa"):
567
+ print(f"\n========== LA Flash DEBUG {label} ==========", flush=True)
568
+ if extra:
569
+ for k, v in extra.items():
570
+ print(f"{k}: {v}", flush=True)
571
+ tail = int(os.environ.get("LA_FLASH_DEBUG_TAIL", "15"))
572
+ bsz, q_len = int(input_ids.shape[0]), int(input_ids.shape[1])
573
+ key_len = int(attention_mask.shape[1])
574
+ q_tail, k_tail = min(tail, q_len), min(tail, key_len)
575
+ print(
576
+ "shapes: "
577
+ f"input_ids={tuple(input_ids.shape)} "
578
+ f"position_ids={tuple(position_ids.shape)} "
579
+ f"attention_mask_key_valid={tuple(attention_mask.shape)} "
580
+ f"mask_2d_q_by_k=({bsz}, {q_len}, {key_len}) "
581
+ f"mask_2d_tail=({bsz}, {q_tail}, {k_tail}) "
582
+ f"past_len={past_len} q_len={q_len} "
583
+ f"mtp_window={mtp_window} ar_decode={not mtp_window}",
584
+ flush=True,
585
+ )
586
+ print(f"dtypes/devices: input_ids={input_ids.dtype}@{input_ids.device} position_ids={position_ids.dtype}@{position_ids.device} attention_mask={attention_mask.dtype}@{attention_mask.device}", flush=True)
587
+ print(f"attention_impl={attention_impl}", flush=True)
588
+ input_rows = _tolist(input_ids)
589
+ pos_rows = _tolist(position_ids)
590
+ print(f"tail_window_last={tail}", flush=True)
591
+ print(f"input_ids_tail.shape=({bsz}, {q_tail})", flush=True)
592
+ print(f"position_ids_tail.shape=({bsz}, {q_tail})", flush=True)
593
+ actual_sdpa = None
594
+ if attention_impl in {"sdpa", "eager", "la_flash"}:
595
+ try:
596
+ actual_sdpa = _actual_sdpa_allowed_masks(model, input_ids, attention_mask, past_len)
597
+ print(
598
+ f"actual_sdpa_4d_mask_shape={actual_sdpa[1]} "
599
+ f"remote_ar_decode={actual_sdpa[2]}",
600
+ flush=True,
601
+ )
602
+ except Exception as e:
603
+ print(f"actual_sdpa_4d_mask_debug_failed={type(e).__name__}: {e}", flush=True)
604
+
605
+ for b in range(input_ids.shape[0]):
606
+ ids_tail = input_rows[b][-tail:]
607
+ pos_tail = pos_rows[b][-tail:]
608
+ allowed = _effective_allowed_mask(attention_mask[b], input_ids.shape[1], past_len, mtp_window)
609
+ q_tail = min(tail, len(allowed))
610
+ k_tail = min(tail, len(allowed[0]) if allowed else 0)
611
+ allowed_tail = _tail_matrix(allowed, rows=q_tail, cols=k_tail)
612
+ print(f"batch_row={b} ar_decode={not mtp_window}", flush=True)
613
+ print(f"input_ids_tail[-{tail}:]: {ids_tail}", flush=True)
614
+ print(f"decoded_tail[-{tail}:]: {_safe_decode_row(tok, ids_tail)}", flush=True)
615
+ print(f"position_ids_tail[-{tail}:]: {pos_tail}", flush=True)
616
+ print(f"expected_mask_2d_tail[-{q_tail}:,-{k_tail}:].shape=({q_tail}, {k_tail})", flush=True)
617
+ print(_format_01_matrix(allowed_tail), flush=True)
618
+ if actual_sdpa is not None:
619
+ actual = actual_sdpa[0][b]
620
+ actual_tail = _tail_matrix(actual, rows=q_tail, cols=k_tail)
621
+ mismatch = sum(
622
+ int(allowed[qi][ki] != actual[qi][ki])
623
+ for qi in range(len(allowed))
624
+ for ki in range(len(allowed[qi]))
625
+ )
626
+ print(
627
+ f"actual_sdpa_mask_2d_tail[-{q_tail}:,-{k_tail}:].shape=({q_tail}, {k_tail})",
628
+ flush=True,
629
+ )
630
+ print(_format_01_matrix(actual_tail), flush=True)
631
+ print(f"expected_vs_actual_sdpa_mismatch_count={mismatch}", flush=True)
632
+
633
+ if _env_flag("LA_FLASH_DEBUG_FULL_MASK", False):
634
+ masks = [
635
+ _effective_allowed_mask(attention_mask[b], input_ids.shape[1], past_len, mtp_window)
636
+ for b in range(input_ids.shape[0])
637
+ ]
638
+ print("effective_allowed_mask_q_by_k_FULL:", masks, flush=True)
639
+ if attention_impl == "magi":
640
+ if bsz == 1:
641
+ print(
642
+ "magi_ranges:",
643
+ _debug_magi_ranges(input_ids.shape[1], past_len, mtp_window),
644
+ flush=True,
645
+ )
646
+ else:
647
+ print(
648
+ "magi_ranges: built once per forward from the batched scheduler mask",
649
+ flush=True,
650
+ )
651
+ print(
652
+ "magi_ranges_single_row_template:",
653
+ _debug_magi_ranges(input_ids.shape[1], past_len, mtp_window),
654
+ flush=True,
655
+ )
656
+
657
+
658
+ def _common_prefix_len(prompt_ids, rows):
659
+ if not rows:
660
+ return 0
661
+ first = prompt_ids[rows[0]]
662
+ max_len = min(int(prompt_ids[r].numel()) for r in rows)
663
+ prefix_len = 0
664
+ for idx in range(max_len):
665
+ val = int(first[idx].item())
666
+ if all(int(prompt_ids[r][idx].item()) == val for r in rows[1:]):
667
+ prefix_len += 1
668
+ else:
669
+ break
670
+ return prefix_len
671
+
672
+
673
+ def _prefill_shared_prefix_kv_rows(model, prompt_ids, vit_list, img_tok, pad, dev, stats=None, debug=False):
674
+ """Cache one common prompt prefix per image-feature group.
675
+
676
+ Multi-category split repeats the same image feature tensor for each
677
+ category prompt. Token ids are identical through the image tokens and the
678
+ fixed prompt prefix, so we prefill that shared prefix once and let each
679
+ category row forward only its text suffix.
680
+ """
681
+ bsz = len(prompt_ids)
682
+ kv_rows = [None] * bsz
683
+ cached_lens = [0] * bsz
684
+ groups = {}
685
+ for row, vit in enumerate(vit_list):
686
+ groups.setdefault(id(vit), []).append(row)
687
+
688
+ items = []
689
+ min_prefix_len = max(1, _env_int("LA_FLASH_SHARED_PREFILL_MIN_PREFIX", 64))
690
+ for rows in groups.values():
691
+ if len(rows) < 2:
692
+ continue
693
+ prefix_len = _common_prefix_len(prompt_ids, rows)
694
+ if prefix_len < min_prefix_len:
695
+ continue
696
+ prefix_ids = prompt_ids[rows[0]][:prefix_len]
697
+ image_token_count = int((prefix_ids == img_tok).sum().item())
698
+ if image_token_count != int(vit_list[rows[0]].shape[0]):
699
+ if debug:
700
+ print(
701
+ "LA Flash shared prefill skip group: "
702
+ f"rows={rows} prefix_len={prefix_len} "
703
+ f"image_tokens={image_token_count} visual_rows={int(vit_list[rows[0]].shape[0])}",
704
+ flush=True,
705
+ )
706
+ continue
707
+ items.append((rows, prefix_ids, vit_list[rows[0]]))
708
+
709
+ if not items:
710
+ return kv_rows, cached_lens
711
+
712
+ lengths = [int(ids.numel()) for _rows, ids, _vit in items]
713
+ pmax = max(lengths)
714
+ input_ids = torch.full((len(items), pmax), pad, dtype=torch.long, device=dev)
715
+ amask = torch.zeros((len(items), pmax), dtype=torch.long, device=dev)
716
+ pos = torch.ones((len(items), pmax), dtype=torch.long, device=dev)
717
+ for item_idx, (_rows, ids, _vit) in enumerate(items):
718
+ length = lengths[item_idx]
719
+ left = pmax - length
720
+ input_ids[item_idx, left:] = ids.to(dev)
721
+ amask[item_idx, left:] = 1
722
+ pos[item_idx, left:] = torch.arange(length, dtype=torch.long, device=dev)
723
+
724
+ visual_features = torch.cat([vit for _rows, _ids, vit in items], dim=0)
725
+ assert int((input_ids == img_tok).sum().item()) == visual_features.shape[0], \
726
+ "shared-prefix image-token count != supplied visual_features rows"
727
+
728
+ if debug:
729
+ group_sizes = [len(rows) for rows, _ids, _vit in items]
730
+ print(
731
+ "LA Flash hybrid shared prompt prefill "
732
+ f"groups={len(items)} group_sizes={group_sizes} prefix_lens={lengths}",
733
+ flush=True,
734
+ )
735
+
736
+ forward_mask, fallback_rows = _forward_attention_mask(
737
+ model, input_ids, amask, 0, mtp_window=False)
738
+ if debug and fallback_rows:
739
+ print(
740
+ "LA Flash hybrid shared prefill safe SDPA fallback "
741
+ f"query_rows={fallback_rows}",
742
+ flush=True,
743
+ )
744
+ forward_kwargs = dict(
745
+ input_ids=input_ids,
746
+ visual_features=visual_features,
747
+ image_token_index=img_tok,
748
+ attention_mask=forward_mask,
749
+ position_ids=pos,
750
+ past_key_values=None,
751
+ use_cache=True,
752
+ )
753
+ if isinstance(forward_mask, dict):
754
+ out = language_model_forward(model, **forward_kwargs, return_logits=False)
755
+ else:
756
+ out = model.language_model.model(**forward_kwargs)
757
+
758
+ real_tokens = sum(lengths)
759
+ shared_rows = sum(len(rows) for rows, _ids, _vit in items)
760
+ saved_tokens = sum((len(rows) - 1) * length for (rows, _ids, _vit), length in zip(items, lengths))
761
+ _record_prefill_stats(
762
+ stats,
763
+ rows=len(items),
764
+ q_len=pmax,
765
+ real_tokens=real_tokens,
766
+ shared_groups=len(items),
767
+ shared_rows=shared_rows,
768
+ saved_tokens=saved_tokens,
769
+ )
770
+
771
+ for item_idx, (rows, _ids, _vit) in enumerate(items):
772
+ prefix_len = lengths[item_idx]
773
+ prefix_kv = _unpack_stock_after_forward(out.past_key_values, item_idx, 0, prefix_len, 0, pmax)
774
+ for row in rows:
775
+ kv_rows[row] = prefix_kv
776
+ cached_lens[row] = prefix_len
777
+
778
+ return kv_rows, cached_lens
779
+
780
+
781
+ @torch.no_grad()
782
+ def _prefill_prompt_kv_rows(model, prompt_ids, vit_list, img_tok, pad, dev, mode, debug=False, stats=None):
783
+ """Return per-row prompt KV caches and cached lengths.
784
+
785
+ ``mode='none'`` preserves the legacy stock-like first MTP forward where the
786
+ whole prompt and the 6-token MTP window are forwarded together. The split
787
+ prefill modes keep prompt KV clean before the scheduler batches only short
788
+ suffix/window forwards, which avoids ragged prompt+window masking in the
789
+ first decode step.
790
+ """
791
+ bsz = len(prompt_ids)
792
+ lengths = [int(p.numel()) for p in prompt_ids]
793
+ if mode == "none":
794
+ return [None] * bsz, [0] * bsz
795
+
796
+ base = model.language_model.model
797
+ if debug:
798
+ print(f"LA Flash hybrid prompt prefill mode={mode} rows={bsz} lengths={lengths}", flush=True)
799
+
800
+ if mode == "shared":
801
+ return _prefill_shared_prefix_kv_rows(
802
+ model, prompt_ids, vit_list, img_tok, pad, dev, stats=stats, debug=debug)
803
+
804
+ if mode == "per_row":
805
+ kv_rows = []
806
+ for b, ids in enumerate(prompt_ids):
807
+ ids = ids.to(dev).unsqueeze(0)
808
+ pos = torch.arange(ids.shape[1], dtype=torch.long, device=dev).unsqueeze(0)
809
+ out = base(
810
+ input_ids=ids,
811
+ visual_features=vit_list[b],
812
+ image_token_index=img_tok,
813
+ attention_mask=None,
814
+ position_ids=pos,
815
+ past_key_values=None,
816
+ use_cache=True,
817
+ )
818
+ kv_rows.append(out.past_key_values)
819
+ _record_prefill_stats(stats, rows=1, q_len=ids.shape[1], real_tokens=ids.shape[1])
820
+ return kv_rows, lengths
821
+
822
+ pmax = max(lengths)
823
+ input_ids = torch.full((bsz, pmax), pad, dtype=torch.long, device=dev)
824
+ amask = torch.zeros((bsz, pmax), dtype=torch.long, device=dev)
825
+ pos = torch.ones((bsz, pmax), dtype=torch.long, device=dev)
826
+ for b, ids in enumerate(prompt_ids):
827
+ left = pmax - lengths[b]
828
+ input_ids[b, left:] = ids.to(dev)
829
+ amask[b, left:] = 1
830
+ pos[b, left:] = torch.arange(lengths[b], dtype=torch.long, device=dev)
831
+
832
+ visual_features = torch.cat(vit_list, dim=0)
833
+ assert int((input_ids == img_tok).sum().item()) == visual_features.shape[0], \
834
+ "image-token count != supplied visual_features rows"
835
+ forward_mask, fallback_rows = _forward_attention_mask(
836
+ model, input_ids, amask, 0, mtp_window=False)
837
+ if debug and fallback_rows:
838
+ print(
839
+ "LA Flash hybrid batch prefill safe SDPA fallback "
840
+ f"query_rows={fallback_rows}",
841
+ flush=True,
842
+ )
843
+ forward_kwargs = dict(
844
+ input_ids=input_ids,
845
+ visual_features=visual_features,
846
+ image_token_index=img_tok,
847
+ attention_mask=forward_mask,
848
+ position_ids=pos,
849
+ past_key_values=None,
850
+ use_cache=True,
851
+ )
852
+ if isinstance(forward_mask, dict):
853
+ out = language_model_forward(model, **forward_kwargs, return_logits=False)
854
+ else:
855
+ out = base(**forward_kwargs)
856
+ _record_prefill_stats(stats, rows=bsz, q_len=pmax, real_tokens=sum(lengths))
857
+ kv_rows = [
858
+ _unpack_stock_after_forward(out.past_key_values, b, 0, lengths[b], 0, pmax)
859
+ for b in range(bsz)
860
+ ]
861
+ return kv_rows, lengths
862
+
863
+
864
+ @torch.no_grad()
865
+ def generate_batch_hybrid(pairs, temperature=README_TEMPERATURE, top_p=README_TOP_P, top_k=None,
866
+ repetition_penalty=README_REPETITION_PENALTY,
867
+ max_new_tokens=README_MAX_NEW_TOKENS, temps=None,
868
+ debug=None, scheduler=None, group_size=None,
869
+ vision_features=None, _stats=None):
870
+ """Batched stock-style LocateAnything-3B hybrid generation.
871
+
872
+ This mirrors ``model.generate(..., generation_mode='hybrid')``: each row
873
+ owns a full ``generated`` token stream plus a KV cache truncated to real
874
+ generated tokens before sampling. MTP forwards
875
+ ``generated[cached_len:] + duplicate-last + mask*5``; AR forwards
876
+ ``generated[cached_len:]``.
877
+ """
878
+ tok, _, model = load()
879
+ san, hpat = _helpers()
880
+ tids = model.token_ids
881
+ img_tok = model.config.image_token_index
882
+ mask_tok = tids["default_mask_token_id"]
883
+ im_end = tids["im_end_token_id"]
884
+ pad = tok.pad_token_id if tok.pad_token_id is not None else im_end
885
+ dev = DEV
886
+
887
+ if not pairs:
888
+ return []
889
+ if temps is not None and len(temps) != len(pairs):
890
+ raise ValueError("temps must have the same length as pairs")
891
+ if vision_features is not None and len(vision_features) != len(pairs):
892
+ raise ValueError("vision_features must have the same length as pairs")
893
+ debug = _debug_enabled(debug)
894
+ scheduler = _hybrid_scheduler(scheduler)
895
+ group_size = _hybrid_group_size(group_size)
896
+ requested_attn = getattr(model, "_la_flash_requested_attn", ATTN_MODE)
897
+ use_magi = requested_attn == "magi"
898
+ prefill_mode = _hybrid_prefill_mode()
899
+ hold_max_steps = max(0, _env_int("LA_FLASH_HYBRID_HOLD_MAX_STEPS", 5))
900
+ adaptive_hold_mtp_max = max(0, _env_int("LA_FLASH_HYBRID_ADAPTIVE_HOLD_MTP_MAX", 3))
901
+ top_level_stats = _stats is None
902
+ if top_level_stats:
903
+ _stats = _new_hybrid_stats(
904
+ len(pairs), scheduler, group_size, hold_max_steps, adaptive_hold_mtp_max)
905
+ if os.environ.get("LA_FLASH_PLAN_STATS", "0") == "1":
906
+ model._la_flash_sparse_plan_stats = None
907
+ if group_size and len(pairs) > group_size:
908
+ outs = []
909
+ if debug:
910
+ print(
911
+ f"LA Flash hybrid grouped scheduling: total_rows={len(pairs)} "
912
+ f"group_size={group_size} scheduler={scheduler} hold_max_steps={hold_max_steps} "
913
+ f"adaptive_hold_mtp_max={adaptive_hold_mtp_max}",
914
+ flush=True,
915
+ )
916
+ for start in range(0, len(pairs), group_size):
917
+ end = min(start + group_size, len(pairs))
918
+ chunk_temps = temps[start:end] if temps is not None else None
919
+ chunk_vision_features = (
920
+ vision_features[start:end] if vision_features is not None else None
921
+ )
922
+ if debug:
923
+ print(f"LA Flash hybrid group rows=[{start}:{end}]", flush=True)
924
+ outs.extend(generate_batch_hybrid(
925
+ pairs[start:end],
926
+ temperature=temperature,
927
+ top_p=top_p,
928
+ top_k=top_k,
929
+ repetition_penalty=repetition_penalty,
930
+ max_new_tokens=max_new_tokens,
931
+ temps=chunk_temps,
932
+ debug=debug,
933
+ scheduler=scheduler,
934
+ group_size=0,
935
+ vision_features=chunk_vision_features,
936
+ _stats=_stats,
937
+ ))
938
+ if top_level_stats:
939
+ _set_last_hybrid_stats(_stats)
940
+ return outs
941
+
942
+ use_cached_tokenize = (
943
+ vision_features is not None
944
+ and os.environ.get("LA_FLASH_CACHE_TOKENIZE", "1") != "0"
945
+ )
946
+ if use_cached_tokenize:
947
+ try:
948
+ prompt_ids = [
949
+ _tokenize_cached_image(q, int(v.shape[0]), im=im)
950
+ for (im, q), v in zip(pairs, vision_features)
951
+ ]
952
+ except Exception as exc:
953
+ if os.environ.get("LA_FLASH_CACHE_TOKENIZE_STRICT", "0") == "1":
954
+ raise
955
+ if debug:
956
+ print(f"LA Flash cached tokenize fallback: {exc}", flush=True)
957
+ prompt_ids = [_tokenize(im, q) for im, q in pairs]
958
+ else:
959
+ prompt_ids = [_tokenize(im, q) for im, q in pairs]
960
+ vit_list = (
961
+ list(vision_features)
962
+ if vision_features is not None
963
+ else _encode_images([im for im, _ in pairs])
964
+ )
965
+ lengths = [int(p.numel()) for p in prompt_ids]
966
+ bsz = len(pairs)
967
+ _record_group_stats(_stats, bsz)
968
+
969
+ _set_llm_mode(model, requested_attn)
970
+
971
+ modes = ["mtp"] * bsz
972
+ finished = [False] * bsz
973
+ gen_ids = [[] for _ in range(bsz)]
974
+ full_ids = [list(ids.detach().cpu().tolist()) for ids in prompt_ids]
975
+ kv_rows, cached_lens = _prefill_prompt_kv_rows(
976
+ model, prompt_ids, vit_list, img_tok, pad, dev, prefill_mode, debug=debug, stats=_stats)
977
+ total_limits = [lengths[b] + max_new_tokens for b in range(bsz)]
978
+
979
+ row_temps = [float(temperature or 0.0)] * bsz if temps is None else [float(t or 0.0) for t in temps]
980
+
981
+ def run_ar(ar_rows, step_idx):
982
+ row_groups = _split_rows_by_kv_budget(ar_rows, kv_rows)
983
+ _record_kv_bucket_stats(_stats, row_groups, kv_rows)
984
+ for row_group in row_groups:
985
+ _step_stock_ar_rows(
986
+ model, san, tids, prompt_ids, kv_rows, row_group,
987
+ cached_lens, full_ids, gen_ids, modes, finished, total_limits,
988
+ pad, img_tok, row_temps, temperature, top_p, top_k,
989
+ repetition_penalty, dev, tok, debug, step_idx, use_magi, _stats,
990
+ )
991
+
992
+ def run_mtp(mtp_rows, step_idx):
993
+ if any(cached_lens[r] == 0 for r in mtp_rows) and any(cached_lens[r] > 0 for r in mtp_rows):
994
+ first_rows = [r for r in mtp_rows if cached_lens[r] == 0]
995
+ cached_rows = [r for r in mtp_rows if cached_lens[r] > 0]
996
+ if first_rows:
997
+ run_mtp(first_rows, step_idx)
998
+ if cached_rows:
999
+ run_mtp(cached_rows, step_idx)
1000
+ return
1001
+ row_groups = _split_rows_by_kv_budget(mtp_rows, kv_rows)
1002
+ _record_kv_bucket_stats(_stats, row_groups, kv_rows)
1003
+ if len(row_groups) > 1:
1004
+ for row_group in row_groups:
1005
+ run_mtp(row_group, step_idx)
1006
+ return
1007
+ _step_stock_mtp_rows(
1008
+ model, san, hpat, tids, prompt_ids, kv_rows, mtp_rows,
1009
+ cached_lens, full_ids, gen_ids, modes, finished, total_limits,
1010
+ vit_list, pad, mask_tok, img_tok, row_temps, top_p, top_k,
1011
+ repetition_penalty, dev, tok, debug, step_idx, use_magi, _stats,
1012
+ )
1013
+
1014
+ def live_rows(mode):
1015
+ return [b for b in range(bsz) if not finished[b] and modes[b] == mode]
1016
+
1017
+ step = 0
1018
+ hold_steps = 0
1019
+ while not all(finished) and step <= max_new_tokens:
1020
+ step += 1
1021
+ if _stats is not None:
1022
+ _stats["decode_loops"] += 1
1023
+ if scheduler == "hold_ar" and hold_max_steps > 0:
1024
+ ar_rows = live_rows("ar")
1025
+ mtp_rows = live_rows("mtp")
1026
+ if ar_rows and mtp_rows and _stats is not None:
1027
+ _stats["mixed_mode_cycles"] += 1
1028
+ if ar_rows and (hold_steps < hold_max_steps or not mtp_rows):
1029
+ if mtp_rows and _stats is not None:
1030
+ _stats["hold_ar_steps"] += 1
1031
+ _stats["hold_ar_held_mtp_rows"] += len(mtp_rows)
1032
+ run_ar(ar_rows, step)
1033
+ hold_steps += 1
1034
+ continue
1035
+ if mtp_rows:
1036
+ if ar_rows and _stats is not None:
1037
+ _stats["hold_ar_limit_mtp_forwards"] += 1
1038
+ run_mtp(mtp_rows, step)
1039
+ hold_steps = 0
1040
+ continue
1041
+
1042
+ if scheduler in {"ar_first", "pipeline", "adaptive"}:
1043
+ ar_rows_at_loop_start = live_rows("ar")
1044
+ mtp_rows_at_loop_start = live_rows("mtp")
1045
+ mixed = bool(ar_rows_at_loop_start and mtp_rows_at_loop_start)
1046
+ if mixed and _stats is not None:
1047
+ _stats["mixed_mode_cycles"] += 1
1048
+
1049
+ if scheduler == "adaptive" and mixed and hold_max_steps > 0:
1050
+ should_hold = len(mtp_rows_at_loop_start) <= adaptive_hold_mtp_max
1051
+ if should_hold and hold_steps < hold_max_steps:
1052
+ if _stats is not None:
1053
+ _stats["adaptive_hold_cycles"] += 1
1054
+ _stats["hold_ar_steps"] += 1
1055
+ _stats["hold_ar_held_mtp_rows"] += len(mtp_rows_at_loop_start)
1056
+ run_ar(ar_rows_at_loop_start, step)
1057
+ hold_steps += 1
1058
+ continue
1059
+
1060
+ if ar_rows_at_loop_start:
1061
+ if mixed and _stats is not None:
1062
+ if scheduler == "adaptive":
1063
+ _stats["adaptive_ar_first_cycles"] += 1
1064
+ else:
1065
+ _stats["ar_first_cycles"] += 1
1066
+ run_ar(ar_rows_at_loop_start, step)
1067
+
1068
+ mtp_rows = live_rows("mtp")
1069
+ if mtp_rows:
1070
+ run_mtp(mtp_rows, step)
1071
+ hold_steps = 0
1072
+
1073
+ if scheduler == "pipeline" and mtp_rows:
1074
+ old_ar = set(ar_rows_at_loop_start)
1075
+ new_ar_rows = [b for b in live_rows("ar") if b not in old_ar]
1076
+ if new_ar_rows:
1077
+ if _stats is not None:
1078
+ _stats["pipeline_ar_after_mtp_cycles"] += 1
1079
+ run_ar(new_ar_rows, step)
1080
+ continue
1081
+
1082
+ mtp_rows = live_rows("mtp")
1083
+ ar_rows_at_loop_start = live_rows("ar")
1084
+ if mtp_rows and ar_rows_at_loop_start and _stats is not None:
1085
+ _stats["mixed_mode_cycles"] += 1
1086
+ if mtp_rows:
1087
+ run_mtp(mtp_rows, step)
1088
+
1089
+ ar_rows = [b for b in range(bsz) if not finished[b] and modes[b] == "ar"]
1090
+ if mtp_rows and ar_rows and _stats is not None:
1091
+ _stats["eager_mtp_then_ar_cycles"] += 1
1092
+ if ar_rows:
1093
+ run_ar(ar_rows, step)
1094
+
1095
+ outs = [
1096
+ tok.decode(torch.tensor(gen_ids[b], dtype=torch.long, device=dev),
1097
+ skip_special_tokens=False) if gen_ids[b] else ""
1098
+ for b in range(bsz)
1099
+ ]
1100
+ if top_level_stats:
1101
+ if os.environ.get("LA_FLASH_PLAN_STATS", "0") == "1":
1102
+ _stats["sparse_plan_stats"] = copy.deepcopy(
1103
+ getattr(model, "_la_flash_sparse_plan_stats", None) or {}
1104
+ )
1105
+ _set_last_hybrid_stats(_stats)
1106
+ return outs
1107
+
1108
+
1109
+ @torch.no_grad()
1110
+ def _step_stock_mtp_rows(model, san, hpat, tids, prompt_ids, kv_rows, rows,
1111
+ cached_lens, full_ids, gen_ids, modes, finished, total_limits,
1112
+ vit_list, pad, mask_tok, img_tok, row_temps, top_p, top_k,
1113
+ repetition_penalty, dev, tok, debug, step_idx, use_magi, stats=None):
1114
+ kv, kvalid, old_lens, kmax = _pack_stock_kv_rows(kv_rows, rows, dev)
1115
+ uncached_lens = [len(full_ids[r]) - cached_lens[r] for r in rows]
1116
+ umax = max(uncached_lens)
1117
+ seq_len = umax + N_FUTURE
1118
+ _record_forward_stats(stats, "mtp", rows, seq_len, uncached_lens)
1119
+
1120
+ suf_ids = torch.full((len(rows), seq_len), pad, dtype=torch.long, device=dev)
1121
+ suf_pos = torch.ones((len(rows), seq_len), dtype=torch.long, device=dev)
1122
+ q_valid = torch.zeros((len(rows), seq_len), dtype=torch.long, device=dev)
1123
+
1124
+ for i, r in enumerate(rows):
1125
+ uncached = full_ids[r][cached_lens[r] :]
1126
+ left = umax - len(uncached)
1127
+ if uncached:
1128
+ suf_ids[i, left : left + len(uncached)] = torch.tensor(uncached, dtype=torch.long, device=dev)
1129
+ suf_pos[i, left : left + len(uncached)] = torch.arange(
1130
+ cached_lens[r], len(full_ids[r]), dtype=torch.long, device=dev)
1131
+ q_valid[i, left : left + len(uncached)] = 1
1132
+
1133
+ rep = full_ids[r][-1]
1134
+ cur_len = len(full_ids[r])
1135
+ suf_ids[i, umax] = rep
1136
+ suf_pos[i, umax] = cur_len - 1
1137
+ q_valid[i, umax] = 1
1138
+ for j in range(1, N_FUTURE):
1139
+ suf_ids[i, umax + j] = mask_tok
1140
+ suf_pos[i, umax + j] = cur_len + (j - 1)
1141
+ q_valid[i, umax + j] = 1
1142
+
1143
+ full_mask = torch.cat([kvalid, q_valid], dim=1)
1144
+
1145
+ if debug:
1146
+ forward_mask, fallback_rows = _forward_attention_mask(
1147
+ model, suf_ids, full_mask, kmax, mtp_window=True, range_plan=True)
1148
+ _print_debug_forward(
1149
+ f"MTP step={step_idx}",
1150
+ model,
1151
+ tok,
1152
+ suf_ids,
1153
+ full_mask,
1154
+ suf_pos,
1155
+ past_len=kmax,
1156
+ mtp_window=True,
1157
+ extra={
1158
+ "global_rows": rows,
1159
+ "old_kv_lens": old_lens,
1160
+ "cached_lens": [cached_lens[r] for r in rows],
1161
+ "full_lens": [len(full_ids[r]) for r in rows],
1162
+ "uncached_lens": uncached_lens,
1163
+ "forward_attention_mask": _mask_desc(forward_mask),
1164
+ "safe_sdpa_fallback_query_rows": fallback_rows,
1165
+ },
1166
+ attention_impl="magi" if use_magi else ATTN_MODE,
1167
+ )
1168
+ else:
1169
+ forward_mask, _ = _forward_attention_mask(
1170
+ model, suf_ids, full_mask, kmax, mtp_window=True, range_plan=True)
1171
+ first_rows = [r for r in rows if cached_lens[r] == 0]
1172
+ visual_features = None
1173
+ if first_rows:
1174
+ if first_rows != rows:
1175
+ raise RuntimeError("mixed first/non-first MTP rows are not supported")
1176
+ visual_features = torch.cat([vit_list[r] for r in rows], dim=0)
1177
+ assert int((suf_ids == img_tok).sum().item()) == visual_features.shape[0], \
1178
+ "image-token count != supplied visual_features rows"
1179
+ out = language_model_forward(
1180
+ model, input_ids=suf_ids, attention_mask=forward_mask,
1181
+ position_ids=suf_pos, past_key_values=kv, use_cache=True,
1182
+ visual_features=visual_features,
1183
+ image_token_index=img_tok if visual_features is not None else None,
1184
+ logits_slice=slice(-N_FUTURE, None))
1185
+
1186
+ for i, r in enumerate(rows):
1187
+ kv_rows[r] = _unpack_stock_after_forward(
1188
+ out.past_key_values, i, old_lens[i], uncached_lens[i], kmax, umax)
1189
+ cached_lens[r] = len(full_ids[r])
1190
+
1191
+ wlogits = out.logits[:, -N_FUTURE:, :]
1192
+ local_prompts = [prompt_ids[r] for r in rows]
1193
+ local_gen = [gen_ids[r] for r in rows]
1194
+ gen_pad = _pad_generated(local_prompts, local_gen, img_tok, dev)
1195
+ per_row_temp = torch.tensor([row_temps[r] for r in rows], dtype=torch.float32, device=dev)
1196
+
1197
+ if BATCH_SAN:
1198
+ x0_all, boxes_all = sample_tokens_batched(
1199
+ wlogits, gen_pad, tids, per_row_temp,
1200
+ repetition_penalty=repetition_penalty, top_p=top_p, top_k=top_k,
1201
+ keep_k_avg=4, generation_mode="hybrid")
1202
+
1203
+ for i, r in enumerate(rows):
1204
+ if finished[r]:
1205
+ continue
1206
+ if BATCH_SAN:
1207
+ x0b, boxb = x0_all[i], boxes_all[i]
1208
+ else:
1209
+ gk = _mk_generate_kwargs(row_temps[r], top_p, top_k, repetition_penalty)
1210
+ _, _, x0, box_avg = san(wlogits[i : i + 1], gen_pad[i : i + 1], tids, keep_k=5, **gk)
1211
+ x0b, boxb = x0[0], box_avg[0]
1212
+ nt = x0b if bool((boxb == 0).all()) else boxb
1213
+ op = hpat(nt, tids, "hybrid")
1214
+
1215
+ toks = [int(t) for t in op["tokens"]]
1216
+ for t in toks:
1217
+ gen_ids[r].append(t)
1218
+ full_ids[r].append(t)
1219
+
1220
+ if op["type"] == "im_end":
1221
+ finished[r] = True
1222
+ elif op["type"] == "error_box":
1223
+ modes[r] = "ar"
1224
+ if len(full_ids[r]) >= total_limits[r]:
1225
+ finished[r] = True
1226
+
1227
+
1228
+ @torch.no_grad()
1229
+ def _step_stock_ar_rows(model, san, tids, prompt_ids, kv_rows, rows,
1230
+ cached_lens, full_ids, gen_ids, modes, finished, total_limits,
1231
+ pad, img_tok, row_temps, temperature, top_p, top_k,
1232
+ repetition_penalty, dev, tok, debug, step_idx, use_magi, stats=None):
1233
+ kv, kvalid, old_lens, kmax = _pack_stock_kv_rows(kv_rows, rows, dev)
1234
+ uncached_lens = [len(full_ids[r]) - cached_lens[r] for r in rows]
1235
+ if any(n <= 0 for n in uncached_lens):
1236
+ raise RuntimeError(f"AR rows have no uncached tokens: {rows}")
1237
+ umax = max(uncached_lens)
1238
+ _record_forward_stats(stats, "ar", rows, umax, uncached_lens)
1239
+
1240
+ suf_ids = torch.full((len(rows), umax), pad, dtype=torch.long, device=dev)
1241
+ suf_pos = torch.ones((len(rows), umax), dtype=torch.long, device=dev)
1242
+ q_valid = torch.zeros((len(rows), umax), dtype=torch.long, device=dev)
1243
+
1244
+ for i, r in enumerate(rows):
1245
+ uncached = full_ids[r][cached_lens[r] :]
1246
+ left = umax - len(uncached)
1247
+ suf_ids[i, left:] = torch.tensor(uncached, dtype=torch.long, device=dev)
1248
+ suf_pos[i, left:] = torch.arange(cached_lens[r], len(full_ids[r]), dtype=torch.long, device=dev)
1249
+ q_valid[i, left:] = 1
1250
+
1251
+ full_mask = torch.cat([kvalid, q_valid], dim=1)
1252
+ if debug:
1253
+ forward_mask, fallback_rows = _forward_attention_mask(
1254
+ model, suf_ids, full_mask, kmax, mtp_window=False, range_plan=True)
1255
+ _print_debug_forward(
1256
+ f"AR step={step_idx}",
1257
+ model,
1258
+ tok,
1259
+ suf_ids,
1260
+ full_mask,
1261
+ suf_pos,
1262
+ past_len=kmax,
1263
+ mtp_window=False,
1264
+ extra={
1265
+ "global_rows": rows,
1266
+ "old_kv_lens": old_lens,
1267
+ "cached_lens": [cached_lens[r] for r in rows],
1268
+ "full_lens": [len(full_ids[r]) for r in rows],
1269
+ "uncached_lens": uncached_lens,
1270
+ "forward_attention_mask": _mask_desc(forward_mask),
1271
+ "safe_sdpa_fallback_query_rows": fallback_rows,
1272
+ },
1273
+ attention_impl="magi" if use_magi else ATTN_MODE,
1274
+ )
1275
+ else:
1276
+ forward_mask, _ = _forward_attention_mask(
1277
+ model, suf_ids, full_mask, kmax, mtp_window=False, range_plan=True)
1278
+
1279
+ out = language_model_forward(
1280
+ model, input_ids=suf_ids, attention_mask=forward_mask,
1281
+ position_ids=suf_pos, past_key_values=kv, use_cache=True,
1282
+ logits_slice=slice(-1, None))
1283
+
1284
+ for i, r in enumerate(rows):
1285
+ kv_rows[r] = _unpack_stock_after_forward(
1286
+ out.past_key_values, i, old_lens[i], uncached_lens[i], kmax, umax)
1287
+ cached_lens[r] = len(full_ids[r])
1288
+
1289
+ if AR_BATCH_SAN:
1290
+ local_prompts = [prompt_ids[r] for r in rows]
1291
+ local_gen = [gen_ids[r] for r in rows]
1292
+ gen_pad = _pad_generated(local_prompts, local_gen, img_tok, dev)
1293
+ per_row_temp = torch.tensor([row_temps[r] for r in rows], dtype=torch.float32, device=dev)
1294
+ x0_all = sample_next_tokens_batched(
1295
+ out.logits[:, -1:, :],
1296
+ gen_pad,
1297
+ per_row_temp,
1298
+ repetition_penalty=repetition_penalty,
1299
+ top_p=top_p,
1300
+ top_k=top_k,
1301
+ )
1302
+
1303
+ for i, r in enumerate(rows):
1304
+ if AR_BATCH_SAN:
1305
+ token_val = int(x0_all[i, 0].item())
1306
+ else:
1307
+ logits = out.logits[i : i + 1, -1:, :]
1308
+ gen_pad = _pad_generated([prompt_ids[r]], [gen_ids[r]], img_tok, dev)
1309
+ gk = _mk_generate_kwargs(temperature, top_p, top_k, repetition_penalty, row_temp=row_temps[r])
1310
+ _, _, x0, _ = san(logits, gen_pad, tids, **gk)
1311
+ token_val = int(x0[0, 0].item())
1312
+ out_type = _classify_ar_token(token_val, tids)
1313
+
1314
+ gen_ids[r].append(token_val)
1315
+ full_ids[r].append(token_val)
1316
+
1317
+ if out_type == "im_end":
1318
+ finished[r] = True
1319
+ elif out_type == "box_end_ar":
1320
+ modes[r] = "mtp"
1321
+
1322
+ if len(full_ids[r]) >= total_limits[r]:
1323
+ finished[r] = True
1324
+
1325
+
1326
+ def generate_batch_grouped_hybrid(groups, temperature=README_TEMPERATURE, top_p=README_TOP_P,
1327
+ top_k=None, repetition_penalty=README_REPETITION_PENALTY,
1328
+ max_new_tokens=README_MAX_NEW_TOKENS, temps=None,
1329
+ debug=None, scheduler=None, group_size=None,
1330
+ vision_features=None):
1331
+ """Hybrid grouped API shape.
1332
+
1333
+ This preserves grouped return shape, but intentionally uses the generic
1334
+ hybrid decoder rather than the fast engine's shared-prefix optimization.
1335
+ """
1336
+ flat = []
1337
+ flat_vision_features = [] if vision_features is not None else None
1338
+ counts = []
1339
+ for group_idx, (im, queries) in enumerate(groups):
1340
+ counts.append(len(queries))
1341
+ flat.extend((im, q) for q in queries)
1342
+ if flat_vision_features is not None:
1343
+ flat_vision_features.extend([vision_features[group_idx]] * len(queries))
1344
+
1345
+ outs = generate_batch_hybrid(
1346
+ flat, temperature=temperature, top_p=top_p, top_k=top_k,
1347
+ repetition_penalty=repetition_penalty, max_new_tokens=max_new_tokens,
1348
+ temps=temps, debug=debug, scheduler=scheduler, group_size=group_size,
1349
+ vision_features=flat_vision_features)
1350
+ res, offset = [], 0
1351
+ for n in counts:
1352
+ res.append(outs[offset : offset + n])
1353
+ offset += n
1354
+ return res
1355
+
1356
+
1357
+ __all__ = ["generate_batch_hybrid", "generate_batch_grouped_hybrid", "get_last_hybrid_stats"]
batch_utils/hybrid_runtime.py ADDED
@@ -0,0 +1,1842 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Internal runtime support for the LocateAnything-3B hybrid batch decoder.
2
+
3
+ This file keeps only the model-loading, tokenization, image-encoding, stock
4
+ processor, and sample-token helpers that ``engine_hybrid.py`` needs.
5
+
6
+ Important env knobs:
7
+ LA_FLASH_MODEL HF repo id / local path of the model (default nvidia/LocateAnything-3B)
8
+ HF_HUB_OFFLINE=1 read the local HF cache only (no network); unset -> download on first use
9
+ LA_FLASH_ATTN sdpa, eager, magi, or la_flash; la_flash uses FlashAttention sparse ranges
10
+ LA_FLASH_STRICT_ATTN 1 -> fail if the requested backend is unavailable;
11
+ default 0 falls back to sdpa
12
+ LA_FLASH_VISION_ATTN auto, flash_attention_2, sdpa, or eager (default auto)
13
+ LA_FLASH_HYBRID_PREFILL shared, none, per_row, or batch prompt KV prefill (default shared)
14
+ MTP_BATCH_VISION 0 -> per-image vision encode (default 1: batched when flash is present)
15
+ LA_FLASH_VISION_ENCODE_BATCH_SIZE
16
+ max images per MoonViT encode micro-batch (default 8; <=0 disables limit)
17
+ MTP_BATCH_SAN 0 -> per-row logits/sample pipeline (default 1: batched over [B,6,V])
18
+ AR_BATCH_SAN 0 -> per-row AR sample pipeline (default 1: batched over [B,1,V])
19
+ """
20
+ import inspect
21
+ import os, warnings, importlib, torch
22
+ from types import SimpleNamespace
23
+ import numpy as np
24
+ from transformers import AutoModel, AutoTokenizer, AutoProcessor
25
+
26
+
27
+ # By default let transformers fetch the model on first use; set HF_HUB_OFFLINE=1 yourself
28
+ # to read the local HF cache only (e.g. air-gapped / already-downloaded runs).
29
+ MODEL = os.environ.get("LA_FLASH_MODEL", "nvidia/LocateAnything-3B")
30
+
31
+
32
+ LLM_ATTN_MODES = ("sdpa", "eager", "magi", "la_flash")
33
+ VISION_ATTN_MODES = ("auto", "flash_attention_2", "sdpa", "eager")
34
+
35
+
36
+ def _normalize_attn_mode(value):
37
+ mode = (value or "sdpa").strip().lower().replace("-", "_")
38
+ aliases = {
39
+ "": "sdpa",
40
+ "manual": "eager",
41
+ "torch": "eager",
42
+ "torch_eager": "eager",
43
+ "torch_sdpa": "sdpa",
44
+ "scaled_dot_product_attention": "sdpa",
45
+ "flash": "la_flash",
46
+ "la_flash": "la_flash",
47
+ "kernel": "la_flash",
48
+ "cuda": "la_flash",
49
+ "range": "la_flash",
50
+ "range_attention": "la_flash",
51
+ "flex_flash": "magi",
52
+ "flex_flash_attention": "magi",
53
+ "flex_flash_attn": "magi",
54
+ }
55
+ mode = aliases.get(mode, mode)
56
+ if mode not in LLM_ATTN_MODES:
57
+ raise ValueError(
58
+ f"LA_FLASH_ATTN must be one of {', '.join(LLM_ATTN_MODES)}; got {value!r}"
59
+ )
60
+ return mode
61
+
62
+
63
+ def _normalize_vision_attn_mode(value):
64
+ mode = (value or "auto").strip().lower().replace("-", "_")
65
+ aliases = {
66
+ "": "auto",
67
+ "flash": "flash_attention_2",
68
+ "flash_attention2": "flash_attention_2",
69
+ "fa2": "flash_attention_2",
70
+ "manual": "eager",
71
+ }
72
+ mode = aliases.get(mode, mode)
73
+ if mode not in VISION_ATTN_MODES:
74
+ raise ValueError(
75
+ f"LA_FLASH_VISION_ATTN must be one of {', '.join(VISION_ATTN_MODES)}; got {value!r}"
76
+ )
77
+ return mode
78
+
79
+
80
+ ATTN_MODE = _normalize_attn_mode(os.environ.get("LA_FLASH_ATTN", "sdpa"))
81
+ REMOTE_ATTN_MODE = "sdpa" if ATTN_MODE in {"la_flash", "magi"} else ATTN_MODE
82
+ VISION_ATTN_MODE = _normalize_vision_attn_mode(os.environ.get("LA_FLASH_VISION_ATTN", "auto"))
83
+ MAX_DIM = 1024
84
+ DEV, DT = "cuda", torch.bfloat16
85
+ N_FUTURE = 6 # = config.block_size (MTP window)
86
+ _PROMPT = "Locate all the instances that matches the following description: "
87
+
88
+
89
+ def _env_flag(name, default=False):
90
+ val = os.environ.get(name)
91
+ if val is None:
92
+ return default
93
+ return val.strip().lower() not in {"0", "false", "no", "off"}
94
+
95
+
96
+ def _env_int(name):
97
+ val = os.environ.get(name)
98
+ if val is None or val.strip() == "":
99
+ return None
100
+ return int(val)
101
+
102
+
103
+ def _strict_attn():
104
+ return _env_flag("LA_FLASH_STRICT_ATTN", False)
105
+
106
+
107
+ def _fallback_to_sdpa(model, requested, reason):
108
+ if requested == "sdpa":
109
+ raise RuntimeError(f"LA_FLASH_ATTN=sdpa failed: {reason}") from reason
110
+ message = f"LA_FLASH_ATTN={requested} is unavailable; falling back to sdpa. Reason: {reason}"
111
+ if _strict_attn():
112
+ raise RuntimeError(message) from reason
113
+ warnings.warn(message)
114
+ _set_llm_mode(model, "sdpa")
115
+ model._la_flash_requested_attn_original = requested
116
+ model._la_flash_attn_fallback_reason = str(reason)
117
+ return "sdpa"
118
+
119
+
120
+ # Optional compile for the shared Qwen2 core. This is off by default because the
121
+ # hybrid scheduler already varies query/cache shapes and first-call compile cost is high.
122
+ MTP_COMPILE = os.environ.get("MTP_COMPILE", "0") == "1"
123
+
124
+ # Batch the MoonViT vision encode across a micro-batch's images: pack N images into ONE
125
+ # extract_feature. With flash present, MoonViT's varlen cu_seqlens path is block-diagonal per
126
+ # image and equivalent to per-image encode.
127
+ # Without flash, sdpa builds a dense [1,S,S] mask -> O(S^2) N^2 -> per-image fallback (auto, see
128
+ # _vision_is_flash). Default ON; set MTP_BATCH_VISION=0 to force per-image.
129
+ BATCH_VISION = os.environ.get("MTP_BATCH_VISION", "1") == "1"
130
+ _vision_encode_batch_size = _env_int("LA_FLASH_VISION_ENCODE_BATCH_SIZE")
131
+ VISION_ENCODE_BATCH_SIZE = 8 if _vision_encode_batch_size is None else max(0, _vision_encode_batch_size)
132
+
133
+ # Batch the per-row box-decode (sample_tokens): run the row-independent logits pipeline
134
+ # (rep-penalty / per-row temperature / top_p / top_k / softmax / sample) ONCE over the whole
135
+ # [B,6,V] step instead of B times on [1,6,V]; only the variable-length box assembly stays per-row.
136
+ # Greedy is BIT-IDENTICAL to the per-row san (argmax, no RNG). Default ON; MTP_BATCH_SAN=0 -> per-row.
137
+ BATCH_SAN = os.environ.get("MTP_BATCH_SAN", "1") == "1"
138
+
139
+ # Batch the AR repair sampler over [B,1,V]. This shares the exact filtering
140
+ # helpers with MTP batching but skips box/ref decoding, so it only replaces the
141
+ # repeated stock one-token sample calls. Sampling itself stays row-ordered by
142
+ # default to preserve the stock RNG consumption pattern for AR repair.
143
+ AR_BATCH_SAN = os.environ.get("AR_BATCH_SAN", "1") == "1"
144
+
145
+ _tok = _proc = _model = None
146
+
147
+ def _magi_diag():
148
+ lines = []
149
+ try:
150
+ import magi_attention
151
+ lines.append(f"magi_attention: OK file={getattr(magi_attention, '__file__', None)}")
152
+ lines.append(f"magi_attention.__version__={getattr(magi_attention, '__version__', '<missing>')}")
153
+ except Exception as e:
154
+ lines.append(f"magi_attention: FAIL {type(e).__name__}: {e}")
155
+ return "\n".join(lines)
156
+ try:
157
+ from magi_attention.functional.flex_flash_attn import flex_flash_attn_func
158
+ lines.append(f"magi_attention.functional.flex_flash_attn: OK func={flex_flash_attn_func}")
159
+ except Exception as e:
160
+ lines.append(f"magi_attention.functional.flex_flash_attn: FAIL {type(e).__name__}: {e}")
161
+ return "\n".join(lines)
162
+
163
+ def _remote_magi_diag(model=None):
164
+ lines = []
165
+ try:
166
+ if model is not None:
167
+ mod = importlib.import_module(type(model.language_model.model).__module__)
168
+ else:
169
+ # Best effort: if the dynamic module is not imported yet this may fail;
170
+ # the post-load diagnostic below will still work.
171
+ mod = importlib.import_module("transformers_modules.LocateAnything-3B.modeling_qwen2")
172
+ lines.append(f"remote_qwen2_module={getattr(mod, '__file__', None)}")
173
+ lines.append(f"remote_qwen2._MAGI_AVAILABLE={getattr(mod, '_MAGI_AVAILABLE', '<missing>')!r}")
174
+ lines.append(f"remote_qwen2.flex_flash_attn_func={getattr(mod, 'flex_flash_attn_func', '<missing>')}")
175
+ except Exception as e:
176
+ lines.append(f"remote_qwen2: diagnostic failed {type(e).__name__}: {e}")
177
+ return "\n".join(lines)
178
+
179
+ def _attn_class_diag(model):
180
+ try:
181
+ llm = model.language_model.model
182
+ classes = [type(layer.self_attn).__name__ for layer in llm.layers[:4]]
183
+ return (
184
+ f"llm._attn_implementation={getattr(llm, '_attn_implementation', None)!r}\n"
185
+ f"config._attn_implementation={getattr(llm.config, '_attn_implementation', None)!r}\n"
186
+ f"first_attn_classes={classes}"
187
+ )
188
+ except Exception as e:
189
+ return f"attention class diagnostic failed {type(e).__name__}: {e}"
190
+
191
+
192
+ def _set_vision_attention_mode(model):
193
+ """Match HF's MoonViT policy: prefer flash_attention_2, then sdpa, then eager."""
194
+ vm = getattr(model, "vision_model", None)
195
+ if vm is None:
196
+ return None
197
+ mod = importlib.import_module(type(vm).__module__)
198
+ funcs = getattr(mod, "VL_VISION_ATTENTION_FUNCTIONS", {})
199
+ has_flash = getattr(mod, "flash_attn_varlen_func", None) is not None
200
+ requested = VISION_ATTN_MODE
201
+
202
+ if requested == "auto":
203
+ candidates = ("flash_attention_2", "sdpa", "eager")
204
+ else:
205
+ candidates = (requested, "flash_attention_2", "sdpa", "eager")
206
+
207
+ chosen = None
208
+ for candidate in candidates:
209
+ if candidate == "flash_attention_2" and not has_flash:
210
+ continue
211
+ if candidate in funcs:
212
+ chosen = candidate
213
+ break
214
+ if chosen is None:
215
+ raise RuntimeError("MoonViT has no supported attention implementation.")
216
+
217
+ if requested == "flash_attention_2" and chosen != "flash_attention_2":
218
+ warnings.warn("LA_FLASH_VISION_ATTN=flash_attention_2 requested but flash-attn is unavailable; "
219
+ f"using {chosen}.")
220
+ elif requested not in {"auto", chosen}:
221
+ warnings.warn(f"LA_FLASH_VISION_ATTN={requested} is unavailable; using {chosen}.")
222
+
223
+ if hasattr(model.config, "vision_config"):
224
+ model.config.vision_config._attn_implementation = chosen
225
+ try:
226
+ vm.config._attn_implementation = chosen
227
+ except Exception:
228
+ pass
229
+ try:
230
+ for block in vm.encoder.blocks:
231
+ block.attn_implementation = chosen
232
+ except Exception as exc:
233
+ raise RuntimeError("Failed to configure MoonViT attention implementation.") from exc
234
+ model._la_flash_vision_attn = chosen
235
+ return chosen
236
+
237
+
238
+ def load():
239
+ """Lazy model load with HF remote-code semantics plus release backends.
240
+
241
+ The text decoder is pinned to one of sdpa/eager/magi/la_flash. MoonViT is
242
+ configured independently and follows the HF policy: flash_attention_2 when
243
+ flash-attn is importable, otherwise sdpa, otherwise eager.
244
+ """
245
+ global _tok, _proc, _model
246
+ if _model is None:
247
+ _tok = AutoTokenizer.from_pretrained(MODEL, trust_remote_code=True)
248
+ _proc = AutoProcessor.from_pretrained(MODEL, trust_remote_code=True)
249
+ attn_impl = REMOTE_ATTN_MODE
250
+ if ATTN_MODE == "magi" and os.environ.get("LA_FLASH_DEBUG", "0") != "0":
251
+ print("LA Flash magi pre-load diagnostic:", flush=True)
252
+ print(_magi_diag(), flush=True)
253
+ _model = AutoModel.from_pretrained(MODEL, torch_dtype=DT, trust_remote_code=True,
254
+ attn_implementation=attn_impl).to(DEV).eval()
255
+ _set_vision_attention_mode(_model)
256
+ actual_attn = getattr(_model.language_model.model, "_attn_implementation", None)
257
+ if ATTN_MODE == "magi" and os.environ.get("LA_FLASH_DEBUG", "0") != "0":
258
+ print("LA Flash magi post-load diagnostic:", flush=True)
259
+ print(_remote_magi_diag(_model), flush=True)
260
+ print(_attn_class_diag(_model), flush=True)
261
+ if ATTN_MODE == "magi":
262
+ try:
263
+ qwen2_mod = importlib.import_module(type(_model.language_model.model).__module__)
264
+ if not getattr(qwen2_mod, "_MAGI_AVAILABLE", False):
265
+ raise RuntimeError(
266
+ "remote module reports _MAGI_AVAILABLE=False.\n"
267
+ f"{_remote_magi_diag(_model)}\n{_magi_diag()}"
268
+ )
269
+ first_attn = type(_model.language_model.model.layers[0].self_attn).__name__
270
+ if actual_attn != "sdpa" or first_attn != "_BatchedMagiAttention":
271
+ _set_llm_mode(_model, "magi")
272
+ actual_attn = getattr(_model.language_model.model, "_attn_implementation", None)
273
+ first_attn = type(_model.language_model.model.layers[0].self_attn).__name__
274
+ if os.environ.get("LA_FLASH_DEBUG", "0") != "0":
275
+ print("LA Flash magi post-swap diagnostic:", flush=True)
276
+ print(_attn_class_diag(_model), flush=True)
277
+ if actual_attn != "sdpa" or first_attn != "_BatchedMagiAttention":
278
+ raise RuntimeError(
279
+ "batched magi attention did not activate. "
280
+ f"actual_attn={actual_attn!r}; first_attn={first_attn!r}; "
281
+ f"{_remote_magi_diag(_model)}; {_attn_class_diag(_model)}"
282
+ )
283
+ _model._la_flash_requested_attn = "magi"
284
+ except Exception as exc:
285
+ _fallback_to_sdpa(_model, "magi", exc)
286
+ else:
287
+ try:
288
+ _set_llm_mode(_model, ATTN_MODE) # decode-safe mask plumbing for sdpa/eager/la_flash
289
+ except Exception as exc:
290
+ _fallback_to_sdpa(_model, ATTN_MODE, exc)
291
+ if MTP_COMPILE:
292
+ _maybe_compile(_model)
293
+ return _tok, _proc, _model
294
+
295
+
296
+ def _maybe_compile(model):
297
+ """Compile the shared Qwen2Model core (base.forward). It backs BOTH prefill (called directly)
298
+ and decode (language_model.forward -> self.model). lm_head + MoonViT left eager. dynamic=True
299
+ so the varying decode S/kvlen don't trigger a recompile storm. No-op + warning if triton is
300
+ missing (inductor needs it on GPU). First call pays the compile cost (~42s warm / ~187s cold)."""
301
+ try:
302
+ import triton # noqa: F401
303
+ except Exception:
304
+ warnings.warn("MTP_COMPILE set but triton is unavailable; running without torch.compile.")
305
+ return
306
+ import torch._dynamo as _dyn
307
+ _dyn.config.cache_size_limit = max(_dyn.config.cache_size_limit, 64)
308
+ base = model.language_model.model
309
+ if not getattr(base, "_mtp_compiled", False):
310
+ base.forward = torch.compile(base.forward, dynamic=True)
311
+ base._mtp_compiled = True
312
+
313
+
314
+ def build_batched_magi_attention_class(mod):
315
+ """Build a Qwen2 attention subclass backed by Magi's flex_flash_attn.
316
+
317
+ The official LocateAnything ``Qwen2MagiAttention`` asserts ``bsz == 1`` and
318
+ relies on ``Qwen2Model._attn_implementation == "magi"`` to build a single
319
+ sample range plan. For release batch inference the hybrid scheduler passes
320
+ a batched Magi range plan directly to this layer; a 4D-mask conversion path
321
+ remains as a compatibility fallback.
322
+ """
323
+ flex_flash_attn_func = getattr(mod, "flex_flash_attn_func", None)
324
+ if flex_flash_attn_func is None:
325
+ try:
326
+ from magi_attention.functional.flex_flash_attn import flex_flash_attn_func
327
+ except Exception as exc:
328
+ raise RuntimeError(
329
+ "LA_FLASH_ATTN=magi requires "
330
+ "magi_attention.functional.flex_flash_attn.flex_flash_attn_func."
331
+ ) from exc
332
+
333
+ FULL, CAUSAL = 0, 1
334
+ causal_plan_cache = {}
335
+ try:
336
+ magi_params = set(inspect.signature(flex_flash_attn_func).parameters)
337
+ except (TypeError, ValueError):
338
+ magi_params = set()
339
+ supports_disable_fwd_atomic = "disable_fwd_atomic_reduction" in magi_params
340
+
341
+ def _disjoint_q_ranges(q_ranges):
342
+ seen = set()
343
+ for start, end in q_ranges:
344
+ key = (int(start), int(end))
345
+ if key in seen:
346
+ return False
347
+ seen.add(key)
348
+ return True
349
+
350
+ def _plan_disjoint_q_ranges(plan):
351
+ cached = plan.get("_la_flash_disjoint_q_ranges")
352
+ if cached is not None:
353
+ return bool(cached)
354
+ q_ranges = plan["q_ranges"].detach().to(device="cpu", dtype=torch.int32).tolist()
355
+ disjoint = _disjoint_q_ranges(q_ranges)
356
+ try:
357
+ plan["_la_flash_disjoint_q_ranges"] = disjoint
358
+ except Exception:
359
+ pass
360
+ return disjoint
361
+
362
+ def _tensor_plan(q_ranges, k_ranges, types, device):
363
+ return {
364
+ "q_ranges": torch.tensor(q_ranges, dtype=torch.int32, device=device).contiguous(),
365
+ "k_ranges": torch.tensor(k_ranges, dtype=torch.int32, device=device).contiguous(),
366
+ "attn_type_map": torch.tensor(types, dtype=torch.int32, device=device).contiguous(),
367
+ "_la_flash_disjoint_q_ranges": _disjoint_q_ranges(q_ranges),
368
+ }
369
+
370
+ def _offset_plan(plan, q_offset, k_offset):
371
+ return (
372
+ (plan["q_ranges"] + int(q_offset)).tolist(),
373
+ (plan["k_ranges"] + int(k_offset)).tolist(),
374
+ plan["attn_type_map"].tolist(),
375
+ )
376
+
377
+ def _causal_plan(bsz, q_len, kv_seq_len, device):
378
+ key = (int(bsz), int(q_len), int(kv_seq_len), device.type, device.index)
379
+ cached = causal_plan_cache.get(key)
380
+ if cached is not None:
381
+ return cached
382
+ q_ranges, k_ranges, types = [], [], []
383
+ for b in range(int(bsz)):
384
+ q_base = b * int(q_len)
385
+ k_base = b * int(kv_seq_len)
386
+ q_ranges.append([q_base, q_base + int(q_len)])
387
+ k_ranges.append([k_base, k_base + int(kv_seq_len)])
388
+ types.append(CAUSAL)
389
+ plan = _tensor_plan(q_ranges, k_ranges, types, device)
390
+ plan.update(
391
+ {
392
+ "flash_cu_seqlens_q": torch.arange(
393
+ 0,
394
+ (int(bsz) + 1) * int(q_len),
395
+ int(q_len),
396
+ dtype=torch.int32,
397
+ device=device,
398
+ ),
399
+ "flash_cu_seqlens_k": torch.arange(
400
+ 0,
401
+ (int(bsz) + 1) * int(kv_seq_len),
402
+ int(kv_seq_len),
403
+ dtype=torch.int32,
404
+ device=device,
405
+ ),
406
+ "flash_causal": True,
407
+ }
408
+ )
409
+ causal_plan_cache[key] = plan
410
+ return plan
411
+
412
+ def _row_segments(row):
413
+ idx = np.flatnonzero(row)
414
+ if idx.size == 0:
415
+ return ((0, 1),)
416
+ split = np.flatnonzero(np.diff(idx) > 1) + 1
417
+ starts = np.concatenate((idx[:1], idx[split]))
418
+ ends = np.concatenate((idx[split - 1], idx[-1:])) + 1
419
+ return tuple((int(s), int(e)) for s, e in zip(starts, ends))
420
+
421
+ def _visible_from_4d_mask(attention_mask, kv_seq_len):
422
+ mask = attention_mask[:, :, :, :kv_seq_len]
423
+ if mask.dtype == torch.bool:
424
+ return mask[:, 0].detach().to(device="cpu", dtype=torch.bool).contiguous()
425
+ mask_cpu = mask[:, 0].detach().to(device="cpu").contiguous()
426
+ if getattr(attention_mask, "_la_flash_visible_mask", False):
427
+ return (mask_cpu > 0).to(dtype=torch.bool)
428
+
429
+ max_value = float(mask_cpu.max().item()) if mask_cpu.numel() else 0.0
430
+ min_value = float(mask_cpu.min().item()) if mask_cpu.numel() else 0.0
431
+ if max_value > 0.0 and min_value >= 0.0:
432
+ return (mask_cpu > 0).to(dtype=torch.bool)
433
+ return (mask_cpu >= 0).to(dtype=torch.bool)
434
+
435
+ def _plan_from_visible_mask(attention_mask, bsz, q_len, kv_seq_len, device):
436
+ cache_key = (int(bsz), int(q_len), int(kv_seq_len), device.type, device.index)
437
+ cached = getattr(attention_mask, "_la_flash_magi_plan", None)
438
+ if cached is not None and cached[0] == cache_key:
439
+ return cached[1]
440
+
441
+ visible = _visible_from_4d_mask(attention_mask, int(kv_seq_len)).numpy()
442
+ q_ranges, k_ranges, types = [], [], []
443
+ for b in range(int(bsz)):
444
+ q_base = b * int(q_len)
445
+ k_base = b * int(kv_seq_len)
446
+ run_start = 0
447
+ run_segments = _row_segments(visible[b, 0])
448
+ for q in range(1, int(q_len)):
449
+ segments = _row_segments(visible[b, q])
450
+ if segments == run_segments:
451
+ continue
452
+ for start, end in run_segments:
453
+ q_ranges.append([q_base + run_start, q_base + q])
454
+ k_ranges.append([k_base + start, k_base + end])
455
+ types.append(FULL)
456
+ run_start = q
457
+ run_segments = segments
458
+ for start, end in run_segments:
459
+ q_ranges.append([q_base + run_start, q_base + int(q_len)])
460
+ k_ranges.append([k_base + start, k_base + end])
461
+ types.append(FULL)
462
+
463
+ plan = _tensor_plan(q_ranges, k_ranges, types, device)
464
+ try:
465
+ attention_mask._la_flash_magi_plan = (cache_key, plan)
466
+ except Exception:
467
+ pass
468
+ return plan
469
+
470
+ def _plan_from_magi_dict(attention_mask, bsz, q_len, kv_seq_len, device):
471
+ if int(bsz) == 1:
472
+ return attention_mask
473
+ q_ranges, k_ranges, types = [], [], []
474
+ for b in range(int(bsz)):
475
+ qs, ks, ts = _offset_plan(
476
+ attention_mask,
477
+ q_offset=b * int(q_len),
478
+ k_offset=b * int(kv_seq_len),
479
+ )
480
+ q_ranges.extend(qs)
481
+ k_ranges.extend(ks)
482
+ types.extend(ts)
483
+ return _tensor_plan(q_ranges, k_ranges, types, device)
484
+
485
+ def _magi_plan(attention_mask, bsz, q_len, kv_seq_len, device):
486
+ if isinstance(attention_mask, dict):
487
+ if attention_mask.get("_la_flash_batched", False):
488
+ return attention_mask
489
+ return _plan_from_magi_dict(attention_mask, bsz, q_len, kv_seq_len, device)
490
+ if attention_mask is None:
491
+ return _causal_plan(bsz, q_len, kv_seq_len, device)
492
+ if attention_mask.size() != (bsz, 1, q_len, kv_seq_len):
493
+ raise ValueError(
494
+ f"Attention mask should be of size {(bsz, 1, q_len, kv_seq_len)}, "
495
+ f"but is {attention_mask.size()}"
496
+ )
497
+ return _plan_from_visible_mask(attention_mask, bsz, q_len, kv_seq_len, device)
498
+
499
+ class _BatchedMagiAttention(mod.Qwen2Attention):
500
+ """MagiAttention path with true batch inference via packed token ranges."""
501
+
502
+ def forward(
503
+ self,
504
+ hidden_states: torch.Tensor,
505
+ attention_mask=None,
506
+ position_ids=None,
507
+ past_key_value=None,
508
+ output_attentions=False,
509
+ use_cache=False,
510
+ **kwargs,
511
+ ):
512
+ if output_attentions:
513
+ raise NotImplementedError("MagiAttention does not support output_attentions=True")
514
+
515
+ bsz, q_len, _ = hidden_states.size()
516
+ query_states = self.q_proj(hidden_states)
517
+ key_states = self.k_proj(hidden_states)
518
+ value_states = self.v_proj(hidden_states)
519
+
520
+ query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
521
+ key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
522
+ value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
523
+
524
+ kv_seq_len = key_states.shape[-2]
525
+ if past_key_value is not None:
526
+ if self.layer_idx is None:
527
+ raise ValueError(
528
+ f"The cache structure has changed since version v4.36. If you are using "
529
+ f"{self.__class__.__name__} for auto-regressive decoding with k/v caching, "
530
+ "please initialize the attention class with a layer index."
531
+ )
532
+ kv_seq_len += past_key_value.get_seq_length(self.layer_idx)
533
+
534
+ cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)
535
+ query_states, key_states = mod.apply_rotary_pos_emb(
536
+ query_states, key_states, cos, sin, position_ids)
537
+
538
+ if past_key_value is not None:
539
+ cache_kwargs = {"sin": sin, "cos": cos}
540
+ key_states, value_states = past_key_value.update(
541
+ key_states, value_states, self.layer_idx, cache_kwargs)
542
+
543
+ kv_seq_len = key_states.shape[-2]
544
+ plan = _magi_plan(attention_mask, bsz, q_len, kv_seq_len, query_states.device)
545
+ magi_extra_kwargs = {}
546
+ if supports_disable_fwd_atomic:
547
+ magi_extra_kwargs["disable_fwd_atomic_reduction"] = (
548
+ (not self.training) and _plan_disjoint_q_ranges(plan)
549
+ )
550
+
551
+ query_states = query_states.transpose(1, 2).reshape(
552
+ bsz * q_len, self.num_heads, self.head_dim).contiguous()
553
+ key_states = key_states.transpose(1, 2).reshape(
554
+ bsz * kv_seq_len, self.num_key_value_heads, self.head_dim).contiguous()
555
+ value_states = value_states.transpose(1, 2).reshape(
556
+ bsz * kv_seq_len, self.num_key_value_heads, self.head_dim).contiguous()
557
+
558
+ attn_output, _ = flex_flash_attn_func(
559
+ query_states,
560
+ key_states,
561
+ value_states,
562
+ q_ranges=plan["q_ranges"],
563
+ k_ranges=plan["k_ranges"],
564
+ attn_type_map=plan["attn_type_map"],
565
+ softmax_scale=getattr(self, "softmax_scale", self.head_dim ** -0.5),
566
+ softcap=0.0,
567
+ deterministic=False,
568
+ **magi_extra_kwargs,
569
+ )
570
+ attn_output = attn_output.view(bsz, q_len, self.hidden_size)
571
+ attn_output = self.o_proj(attn_output)
572
+ return attn_output, None, past_key_value
573
+
574
+ return _BatchedMagiAttention
575
+
576
+
577
+ def build_la_flash_attention_class(mod):
578
+ """Build a Qwen2 attention subclass backed by LA Flash sparse ranges."""
579
+ try:
580
+ from kernel_utils import is_available, range_attention
581
+ except Exception as exc:
582
+ raise RuntimeError(
583
+ "LA_FLASH_ATTN=la_flash requires kernel_utils and FlashAttention."
584
+ ) from exc
585
+ if not is_available():
586
+ raise RuntimeError(
587
+ "LA_FLASH_ATTN=la_flash requires flash_attn.flash_attn_varlen_func."
588
+ )
589
+
590
+ FULL, CAUSAL = 0, 1
591
+ causal_plan_cache = {}
592
+
593
+ def _tensor_plan(q_ranges, k_ranges, types, device):
594
+ max_q_len = max((int(end) - int(start) for start, end in q_ranges), default=0)
595
+ max_k_len = max((int(end) - int(start) for start, end in k_ranges), default=0)
596
+ plan = {
597
+ "q_ranges": torch.tensor(q_ranges, dtype=torch.int32, device=device).contiguous(),
598
+ "k_ranges": torch.tensor(k_ranges, dtype=torch.int32, device=device).contiguous(),
599
+ "attn_type_map": torch.tensor(types, dtype=torch.int32, device=device).contiguous(),
600
+ "max_q_len": max_q_len,
601
+ "max_k_len": max_k_len,
602
+ }
603
+ plan.update(_la_flash_group_plan_tensors(q_ranges, types, device))
604
+ return plan
605
+
606
+ def _offset_plan(plan, q_offset, k_offset):
607
+ return (
608
+ (plan["q_ranges"] + int(q_offset)).tolist(),
609
+ (plan["k_ranges"] + int(k_offset)).tolist(),
610
+ plan["attn_type_map"].tolist(),
611
+ )
612
+
613
+ def _causal_plan(bsz, q_len, kv_seq_len, device):
614
+ key = (int(bsz), int(q_len), int(kv_seq_len), device.type, device.index)
615
+ cached = causal_plan_cache.get(key)
616
+ if cached is not None:
617
+ return cached
618
+ q_ranges, k_ranges, types = [], [], []
619
+ for b in range(int(bsz)):
620
+ q_base = b * int(q_len)
621
+ k_base = b * int(kv_seq_len)
622
+ q_ranges.append([q_base, q_base + int(q_len)])
623
+ k_ranges.append([k_base, k_base + int(kv_seq_len)])
624
+ types.append(CAUSAL)
625
+ plan = _tensor_plan(q_ranges, k_ranges, types, device)
626
+ plan.update(
627
+ {
628
+ "flash_cu_seqlens_q": torch.arange(
629
+ 0,
630
+ (int(bsz) + 1) * int(q_len),
631
+ int(q_len),
632
+ dtype=torch.int32,
633
+ device=device,
634
+ ),
635
+ "flash_cu_seqlens_k": torch.arange(
636
+ 0,
637
+ (int(bsz) + 1) * int(kv_seq_len),
638
+ int(kv_seq_len),
639
+ dtype=torch.int32,
640
+ device=device,
641
+ ),
642
+ "flash_causal": True,
643
+ }
644
+ )
645
+ causal_plan_cache[key] = plan
646
+ return plan
647
+
648
+ def _row_segments(row):
649
+ idx = np.flatnonzero(row)
650
+ if idx.size == 0:
651
+ return ((0, 1),)
652
+ split = np.flatnonzero(np.diff(idx) > 1) + 1
653
+ starts = np.concatenate((idx[:1], idx[split]))
654
+ ends = np.concatenate((idx[split - 1], idx[-1:])) + 1
655
+ return tuple((int(s), int(e)) for s, e in zip(starts, ends))
656
+
657
+ def _visible_from_4d_mask(attention_mask, kv_seq_len):
658
+ mask = attention_mask[:, :, :, :kv_seq_len]
659
+ if mask.dtype == torch.bool:
660
+ return mask[:, 0].detach().to(device="cpu", dtype=torch.bool).contiguous()
661
+ mask_cpu = mask[:, 0].detach().to(device="cpu").contiguous()
662
+ if getattr(attention_mask, "_la_flash_visible_mask", False):
663
+ return (mask_cpu > 0).to(dtype=torch.bool)
664
+
665
+ max_value = float(mask_cpu.max().item()) if mask_cpu.numel() else 0.0
666
+ min_value = float(mask_cpu.min().item()) if mask_cpu.numel() else 0.0
667
+ if max_value > 0.0 and min_value >= 0.0:
668
+ return (mask_cpu > 0).to(dtype=torch.bool)
669
+ return (mask_cpu >= 0).to(dtype=torch.bool)
670
+
671
+ def _prefix_len(row):
672
+ idx = np.flatnonzero(row)
673
+ if idx.size == 0:
674
+ return None
675
+ end = int(idx[-1]) + 1
676
+ if not bool(row[:end].all()) or bool(row[end:].any()):
677
+ return None
678
+ return end
679
+
680
+ def _causal_plan_from_visible(visible, bsz, q_len, kv_seq_len, device):
681
+ q_ranges, k_ranges, types = [], [], []
682
+ packed_flash = True
683
+ for b in range(int(bsz)):
684
+ first_len = _prefix_len(visible[b, 0])
685
+ if first_len is None:
686
+ return None
687
+ valid_len = int(first_len) + int(q_len) - 1
688
+ if valid_len < int(q_len) or valid_len > int(kv_seq_len):
689
+ return None
690
+ for q in range(int(q_len)):
691
+ row_len = _prefix_len(visible[b, q])
692
+ expected = valid_len - int(q_len) + q + 1
693
+ if row_len != expected:
694
+ return None
695
+ q_base = b * int(q_len)
696
+ k_base = b * int(kv_seq_len)
697
+ q_ranges.append([q_base, q_base + int(q_len)])
698
+ k_ranges.append([k_base, k_base + valid_len])
699
+ types.append(CAUSAL)
700
+ packed_flash = packed_flash and valid_len == int(kv_seq_len)
701
+
702
+ plan = _tensor_plan(q_ranges, k_ranges, types, device)
703
+ plan["_la_flash_disjoint_q_ranges"] = True
704
+ if packed_flash:
705
+ plan.update(
706
+ {
707
+ "flash_cu_seqlens_q": torch.arange(
708
+ 0,
709
+ (int(bsz) + 1) * int(q_len),
710
+ int(q_len),
711
+ dtype=torch.int32,
712
+ device=device,
713
+ ),
714
+ "flash_cu_seqlens_k": torch.arange(
715
+ 0,
716
+ (int(bsz) + 1) * int(kv_seq_len),
717
+ int(kv_seq_len),
718
+ dtype=torch.int32,
719
+ device=device,
720
+ ),
721
+ "flash_causal": True,
722
+ }
723
+ )
724
+ return plan
725
+
726
+ def _plan_from_visible_mask(attention_mask, bsz, q_len, kv_seq_len, device):
727
+ cache_key = (int(bsz), int(q_len), int(kv_seq_len), device.type, device.index, "la_flash")
728
+ cached = getattr(attention_mask, "_la_flash_range_plan", None)
729
+ if cached is not None and cached[0] == cache_key:
730
+ return cached[1]
731
+
732
+ visible = _visible_from_4d_mask(attention_mask, int(kv_seq_len)).numpy()
733
+ plan = _causal_plan_from_visible(visible, bsz, q_len, kv_seq_len, device)
734
+ if plan is not None:
735
+ try:
736
+ attention_mask._la_flash_range_plan = (cache_key, plan)
737
+ except Exception:
738
+ pass
739
+ return plan
740
+
741
+ q_ranges, k_ranges, types = [], [], []
742
+ for b in range(int(bsz)):
743
+ q_base = b * int(q_len)
744
+ k_base = b * int(kv_seq_len)
745
+ run_start = 0
746
+ run_segments = _row_segments(visible[b, 0])
747
+ for q in range(1, int(q_len)):
748
+ segments = _row_segments(visible[b, q])
749
+ if segments == run_segments:
750
+ continue
751
+ for start, end in run_segments:
752
+ q_ranges.append([q_base + run_start, q_base + q])
753
+ k_ranges.append([k_base + start, k_base + end])
754
+ types.append(FULL)
755
+ run_start = q
756
+ run_segments = segments
757
+ for start, end in run_segments:
758
+ q_ranges.append([q_base + run_start, q_base + int(q_len)])
759
+ k_ranges.append([k_base + start, k_base + end])
760
+ types.append(FULL)
761
+
762
+ plan = _tensor_plan(q_ranges, k_ranges, types, device)
763
+ try:
764
+ attention_mask._la_flash_range_plan = (cache_key, plan)
765
+ except Exception:
766
+ pass
767
+ return plan
768
+
769
+ def _plan_from_magi_dict(attention_mask, bsz, q_len, kv_seq_len, device):
770
+ if int(bsz) == 1:
771
+ return attention_mask
772
+ q_ranges, k_ranges, types = [], [], []
773
+ for b in range(int(bsz)):
774
+ qs, ks, ts = _offset_plan(
775
+ attention_mask,
776
+ q_offset=b * int(q_len),
777
+ k_offset=b * int(kv_seq_len),
778
+ )
779
+ q_ranges.extend(qs)
780
+ k_ranges.extend(ks)
781
+ types.extend(ts)
782
+ return _tensor_plan(q_ranges, k_ranges, types, device)
783
+
784
+ def _range_plan(attention_mask, bsz, q_len, kv_seq_len, device):
785
+ if isinstance(attention_mask, dict):
786
+ if attention_mask.get("_la_flash_batched", False):
787
+ return attention_mask
788
+ return _plan_from_magi_dict(attention_mask, bsz, q_len, kv_seq_len, device)
789
+ if attention_mask is None:
790
+ return _causal_plan(bsz, q_len, kv_seq_len, device)
791
+ if attention_mask.size() != (bsz, 1, q_len, kv_seq_len):
792
+ raise ValueError(
793
+ f"Attention mask should be of size {(bsz, 1, q_len, kv_seq_len)}, "
794
+ f"but is {attention_mask.size()}"
795
+ )
796
+ return _plan_from_visible_mask(attention_mask, bsz, q_len, kv_seq_len, device)
797
+
798
+ class _LaFlashAttention(mod.Qwen2Attention):
799
+ """Range-plan attention path backed by FlashAttention sparse ranges."""
800
+
801
+ def forward(
802
+ self,
803
+ hidden_states: torch.Tensor,
804
+ attention_mask=None,
805
+ position_ids=None,
806
+ past_key_value=None,
807
+ output_attentions=False,
808
+ use_cache=False,
809
+ **kwargs,
810
+ ):
811
+ if output_attentions:
812
+ raise NotImplementedError("LA Flash attention does not support output_attentions=True")
813
+
814
+ bsz, q_len, _ = hidden_states.size()
815
+ query_states = self.q_proj(hidden_states)
816
+ key_states = self.k_proj(hidden_states)
817
+ value_states = self.v_proj(hidden_states)
818
+
819
+ query_states = query_states.view(bsz, q_len, self.num_heads, self.head_dim).transpose(1, 2)
820
+ key_states = key_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
821
+ value_states = value_states.view(bsz, q_len, self.num_key_value_heads, self.head_dim).transpose(1, 2)
822
+
823
+ kv_seq_len = key_states.shape[-2]
824
+ if past_key_value is not None:
825
+ if self.layer_idx is None:
826
+ raise ValueError(
827
+ f"The cache structure has changed since version v4.36. If you are using "
828
+ f"{self.__class__.__name__} for auto-regressive decoding with k/v caching, "
829
+ "please initialize the attention class with a layer index."
830
+ )
831
+ kv_seq_len += past_key_value.get_seq_length(self.layer_idx)
832
+
833
+ cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)
834
+ query_states, key_states = mod.apply_rotary_pos_emb(
835
+ query_states, key_states, cos, sin, position_ids)
836
+
837
+ if past_key_value is not None:
838
+ cache_kwargs = {"sin": sin, "cos": cos}
839
+ key_states, value_states = past_key_value.update(
840
+ key_states, value_states, self.layer_idx, cache_kwargs)
841
+
842
+ kv_seq_len = key_states.shape[-2]
843
+ dense_backend = os.environ.get("LA_FLASH_DENSE_BACKEND", "sdpa").strip().lower()
844
+ if dense_backend == "sdpa" and not isinstance(attention_mask, dict):
845
+ dense_key_states = mod.repeat_kv(key_states, self.num_key_value_groups)
846
+ dense_value_states = mod.repeat_kv(value_states, self.num_key_value_groups)
847
+ if attention_mask is not None:
848
+ if attention_mask.size() != (bsz, 1, q_len, kv_seq_len):
849
+ raise ValueError(
850
+ f"Attention mask should be of size {(bsz, 1, q_len, kv_seq_len)}, "
851
+ f"but is {attention_mask.size()}"
852
+ )
853
+ query_for_sdpa = query_states.contiguous()
854
+ key_for_sdpa = dense_key_states.contiguous()
855
+ value_for_sdpa = dense_value_states.contiguous()
856
+ is_causal = False
857
+ elif past_key_value is None:
858
+ query_for_sdpa = query_states
859
+ key_for_sdpa = dense_key_states
860
+ value_for_sdpa = dense_value_states
861
+ is_causal = bool(self.is_causal and q_len > 1)
862
+ else:
863
+ query_for_sdpa = key_for_sdpa = value_for_sdpa = None
864
+ is_causal = False
865
+ if query_for_sdpa is not None:
866
+ attn_output = torch.nn.functional.scaled_dot_product_attention(
867
+ query_for_sdpa,
868
+ key_for_sdpa,
869
+ value_for_sdpa,
870
+ attn_mask=attention_mask,
871
+ dropout_p=self.attention_dropout if self.training else 0.0,
872
+ is_causal=is_causal,
873
+ )
874
+ attn_output = attn_output.transpose(1, 2).contiguous()
875
+ attn_output = attn_output.reshape(bsz, q_len, self.hidden_size)
876
+ attn_output = self.o_proj(attn_output)
877
+ return attn_output, None, past_key_value
878
+
879
+ plan = _range_plan(attention_mask, bsz, q_len, kv_seq_len, query_states.device)
880
+
881
+ query_states = query_states.transpose(1, 2).reshape(
882
+ bsz * q_len, self.num_heads, self.head_dim).contiguous()
883
+ key_states = key_states.transpose(1, 2).reshape(
884
+ bsz * kv_seq_len, self.num_key_value_heads, self.head_dim).contiguous()
885
+ value_states = value_states.transpose(1, 2).reshape(
886
+ bsz * kv_seq_len, self.num_key_value_heads, self.head_dim).contiguous()
887
+
888
+ attn_output = range_attention(
889
+ query_states,
890
+ key_states,
891
+ value_states,
892
+ plan["q_ranges"],
893
+ plan["k_ranges"],
894
+ plan["attn_type_map"],
895
+ getattr(self, "softmax_scale", self.head_dim ** -0.5),
896
+ segment_offsets=plan.get("segment_offsets"),
897
+ group_q_ranges=plan.get("group_q_ranges"),
898
+ group_attn_type_map=plan.get("group_attn_type_map"),
899
+ max_q_len=plan.get("max_q_len"),
900
+ max_k_len=plan.get("max_k_len"),
901
+ flash_cu_seqlens_q=plan.get("flash_cu_seqlens_q"),
902
+ flash_cu_seqlens_k=plan.get("flash_cu_seqlens_k"),
903
+ flash_causal=plan.get("flash_causal"),
904
+ disjoint_q_ranges=plan.get("_la_flash_disjoint_q_ranges"),
905
+ )
906
+ attn_output = attn_output.view(bsz, q_len, self.hidden_size)
907
+ attn_output = self.o_proj(attn_output)
908
+ return attn_output, None, past_key_value
909
+
910
+ return _LaFlashAttention
911
+
912
+
913
+ def _is_magi_plan(obj):
914
+ return isinstance(obj, dict) and {
915
+ "q_ranges",
916
+ "k_ranges",
917
+ "attn_type_map",
918
+ }.issubset(obj.keys())
919
+
920
+
921
+ def _la_flash_group_plan_tensors(q_ranges, types, device):
922
+ """Group consecutive Magi range entries that share the same query span.
923
+
924
+ Magi-style plans may represent one query span with multiple disjoint key
925
+ spans. LA Flash consumes those as one FlashAttention-backed softmax group.
926
+ """
927
+ if not q_ranges:
928
+ return {
929
+ "group_q_ranges": torch.empty((0, 2), dtype=torch.int32, device=device),
930
+ "segment_offsets": torch.zeros((1,), dtype=torch.int32, device=device),
931
+ "group_attn_type_map": torch.empty((0,), dtype=torch.int32, device=device),
932
+ }
933
+
934
+ grouped_q, grouped_types, offsets = [], [], [0]
935
+ last_q = None
936
+ last_type = None
937
+ for idx, (q_range, attn_type) in enumerate(zip(q_ranges, types)):
938
+ key = (int(q_range[0]), int(q_range[1]))
939
+ attn_type = int(attn_type)
940
+ if last_q is None:
941
+ grouped_q.append([key[0], key[1]])
942
+ grouped_types.append(attn_type)
943
+ last_q = key
944
+ last_type = attn_type
945
+ continue
946
+ if key == last_q and attn_type == last_type:
947
+ continue
948
+ offsets.append(idx)
949
+ grouped_q.append([key[0], key[1]])
950
+ grouped_types.append(attn_type)
951
+ last_q = key
952
+ last_type = attn_type
953
+ offsets.append(len(q_ranges))
954
+
955
+ return {
956
+ "group_q_ranges": torch.tensor(grouped_q, dtype=torch.int32, device=device).contiguous(),
957
+ "segment_offsets": torch.tensor(offsets, dtype=torch.int32, device=device).contiguous(),
958
+ "group_attn_type_map": torch.tensor(grouped_types, dtype=torch.int32, device=device).contiguous(),
959
+ "max_q_len": max((end - start for start, end in grouped_q), default=0),
960
+ }
961
+
962
+
963
+ def _record_sparse_plan_stats(model, q_ranges, k_ranges, types):
964
+ if os.environ.get("LA_FLASH_PLAN_STATS", "0") != "1":
965
+ return
966
+ stats = getattr(model, "_la_flash_sparse_plan_stats", None)
967
+ if stats is None:
968
+ stats = {
969
+ "calls": 0,
970
+ "ranges": 0,
971
+ "q_tokens": 0,
972
+ "k_tokens": 0,
973
+ "max_q_len": 0,
974
+ "max_k_len": 0,
975
+ "full_ranges": 0,
976
+ "causal_ranges": 0,
977
+ "other_ranges": 0,
978
+ }
979
+ model._la_flash_sparse_plan_stats = stats
980
+ stats["calls"] += 1
981
+ stats["ranges"] += len(q_ranges)
982
+ for (q_start, q_end), (k_start, k_end), attn_type in zip(q_ranges, k_ranges, types):
983
+ q_len = int(q_end) - int(q_start)
984
+ k_len = int(k_end) - int(k_start)
985
+ stats["q_tokens"] += q_len
986
+ stats["k_tokens"] += k_len
987
+ stats["max_q_len"] = max(stats["max_q_len"], q_len)
988
+ stats["max_k_len"] = max(stats["max_k_len"], k_len)
989
+ attn_type = int(attn_type)
990
+ if attn_type == 0:
991
+ stats["full_ranges"] += 1
992
+ elif attn_type == 1:
993
+ stats["causal_ranges"] += 1
994
+ else:
995
+ stats["other_ranges"] += 1
996
+
997
+
998
+ def build_magi_scheduler_ranges(model, attention_mask_2d, input_ids, past_len, mtp_window=False):
999
+ """Build batched Magi ranges directly from the hybrid scheduler mask.
1000
+
1001
+ The official Qwen2 SDPA dispatcher may optimize an all-valid 2D mask to
1002
+ ``None`` before decoder layers see it. That is correct for plain causal
1003
+ attention but loses LocateAnything's MTP generation-window rule. Building
1004
+ ranges here keeps Magi batch inference exact and avoids per-layer dense
1005
+ mask conversion.
1006
+ """
1007
+ requested_attn = getattr(model, "_la_flash_requested_attn", ATTN_MODE)
1008
+ if requested_attn not in {"magi", "la_flash"}:
1009
+ return None
1010
+ if attention_mask_2d is None or not hasattr(attention_mask_2d, "dim") or attention_mask_2d.dim() != 2:
1011
+ return None
1012
+
1013
+ bsz, q_len = int(input_ids.shape[0]), int(input_ids.shape[1])
1014
+ key_len = int(attention_mask_2d.shape[1])
1015
+ dev = input_ids.device
1016
+ llm = model.language_model.model
1017
+ block = int(getattr(llm, "block_size", N_FUTURE))
1018
+ causal_attn = bool(getattr(llm, "causal_attn", False))
1019
+ use_mtp_window = bool(mtp_window and q_len >= block and key_len >= block)
1020
+ q0 = max(0, q_len - block)
1021
+ k0 = max(0, key_len - block)
1022
+ blocked_k = k0 - 1
1023
+ past_len = int(past_len)
1024
+
1025
+ key_valid = attention_mask_2d.detach().to(device="cpu", dtype=torch.bool).contiguous().numpy()
1026
+ key_idx = np.arange(key_len)
1027
+ q_ranges, k_ranges, types = [], [], []
1028
+ if not use_mtp_window:
1029
+ causal_q_ranges, causal_k_ranges, causal_types = [], [], []
1030
+ causal_fast_path = True
1031
+ packed_flash = True
1032
+ for b in range(bsz):
1033
+ valid = np.flatnonzero(key_valid[b])
1034
+ if valid.size == 0:
1035
+ causal_fast_path = False
1036
+ break
1037
+ valid_len = int(valid[-1]) + 1
1038
+ if valid_len < q_len or not bool(key_valid[b, :valid_len].all()) or bool(key_valid[b, valid_len:].any()):
1039
+ causal_fast_path = False
1040
+ break
1041
+ packed_flash = packed_flash and valid_len == key_len
1042
+ q_base = b * q_len
1043
+ k_base = b * key_len
1044
+ causal_q_ranges.append([q_base, q_base + q_len])
1045
+ causal_k_ranges.append([k_base, k_base + valid_len])
1046
+ causal_types.append(1)
1047
+ if causal_fast_path:
1048
+ plan = {
1049
+ "q_ranges": torch.tensor(causal_q_ranges, dtype=torch.int32, device=dev).contiguous(),
1050
+ "k_ranges": torch.tensor(causal_k_ranges, dtype=torch.int32, device=dev).contiguous(),
1051
+ "attn_type_map": torch.tensor(causal_types, dtype=torch.int32, device=dev).contiguous(),
1052
+ "max_q_len": q_len,
1053
+ "max_k_len": max((end - start for start, end in causal_k_ranges), default=0),
1054
+ "_la_flash_batched": True,
1055
+ "_la_flash_disjoint_q_ranges": True,
1056
+ }
1057
+ if packed_flash:
1058
+ plan.update(
1059
+ {
1060
+ "flash_cu_seqlens_q": torch.arange(
1061
+ 0,
1062
+ (bsz + 1) * q_len,
1063
+ q_len,
1064
+ dtype=torch.int32,
1065
+ device=dev,
1066
+ ),
1067
+ "flash_cu_seqlens_k": torch.arange(
1068
+ 0,
1069
+ (bsz + 1) * key_len,
1070
+ key_len,
1071
+ dtype=torch.int32,
1072
+ device=dev,
1073
+ ),
1074
+ "flash_causal": True,
1075
+ }
1076
+ )
1077
+ plan.update(_la_flash_group_plan_tensors(causal_q_ranges, causal_types, dev))
1078
+ _record_sparse_plan_stats(model, causal_q_ranges, causal_k_ranges, causal_types)
1079
+ return plan
1080
+
1081
+ def row_segments(row):
1082
+ idx = np.flatnonzero(row)
1083
+ if idx.size == 0:
1084
+ return ((0, 1),)
1085
+ split = np.flatnonzero(np.diff(idx) > 1) + 1
1086
+ starts = np.concatenate((idx[:1], idx[split]))
1087
+ ends = np.concatenate((idx[split - 1], idx[-1:])) + 1
1088
+ return tuple((int(s), int(e)) for s, e in zip(starts, ends))
1089
+
1090
+ for b in range(bsz):
1091
+ q_base = b * q_len
1092
+ k_base = b * key_len
1093
+ run_start = 0
1094
+ run_segments = None
1095
+ if use_mtp_window and not causal_attn:
1096
+ prefix_q_len = q0
1097
+ prefix_k_end = past_len + prefix_q_len
1098
+ prefix_ok = (
1099
+ prefix_q_len > 0
1100
+ and prefix_k_end <= key_len
1101
+ and bool(key_valid[b, :prefix_k_end].all())
1102
+ )
1103
+ window_prefix_ok = blocked_k <= 0 or bool(key_valid[b, :blocked_k].all())
1104
+ window_ok = bool(key_valid[b, k0:key_len].all())
1105
+ if prefix_ok:
1106
+ q_ranges.append([q_base, q_base + prefix_q_len])
1107
+ k_ranges.append([k_base, k_base + prefix_k_end])
1108
+ types.append(1)
1109
+ run_start = prefix_q_len
1110
+ if run_start == prefix_q_len and prefix_q_len < q_len and window_prefix_ok and window_ok:
1111
+ if blocked_k > 0:
1112
+ q_ranges.append([q_base + prefix_q_len, q_base + q_len])
1113
+ k_ranges.append([k_base, k_base + blocked_k])
1114
+ types.append(0)
1115
+ q_ranges.append([q_base + prefix_q_len, q_base + q_len])
1116
+ k_ranges.append([k_base + k0, k_base + key_len])
1117
+ types.append(0)
1118
+ continue
1119
+
1120
+ for q in range(run_start, q_len):
1121
+ visible = key_valid[b] & (key_idx <= q + past_len)
1122
+ if use_mtp_window and q >= q0:
1123
+ if not causal_attn:
1124
+ visible = visible.copy()
1125
+ visible[k0:key_len] = key_valid[b, k0:key_len]
1126
+ if blocked_k >= 0:
1127
+ if visible.base is None:
1128
+ visible[blocked_k] = False
1129
+ else:
1130
+ visible = visible.copy()
1131
+ visible[blocked_k] = False
1132
+ segments = row_segments(visible)
1133
+ if run_segments is None:
1134
+ run_segments = segments
1135
+ continue
1136
+ if segments == run_segments:
1137
+ continue
1138
+ for start, end in run_segments:
1139
+ q_ranges.append([q_base + run_start, q_base + q])
1140
+ k_ranges.append([k_base + start, k_base + end])
1141
+ types.append(0)
1142
+ run_start = q
1143
+ run_segments = segments
1144
+ for start, end in run_segments:
1145
+ q_ranges.append([q_base + run_start, q_base + q_len])
1146
+ k_ranges.append([k_base + start, k_base + end])
1147
+ types.append(0)
1148
+
1149
+ seen_q_ranges = set()
1150
+ disjoint_q_ranges = True
1151
+ for start, end in q_ranges:
1152
+ key = (int(start), int(end))
1153
+ if key in seen_q_ranges:
1154
+ disjoint_q_ranges = False
1155
+ break
1156
+ seen_q_ranges.add(key)
1157
+
1158
+ plan = {
1159
+ "q_ranges": torch.tensor(q_ranges, dtype=torch.int32, device=dev).contiguous(),
1160
+ "k_ranges": torch.tensor(k_ranges, dtype=torch.int32, device=dev).contiguous(),
1161
+ "attn_type_map": torch.tensor(types, dtype=torch.int32, device=dev).contiguous(),
1162
+ "max_q_len": max((end - start for start, end in q_ranges), default=0),
1163
+ "max_k_len": max((end - start for start, end in k_ranges), default=0),
1164
+ "_la_flash_batched": True,
1165
+ "_la_flash_disjoint_q_ranges": disjoint_q_ranges,
1166
+ }
1167
+ plan.update(_la_flash_group_plan_tensors(q_ranges, types, dev))
1168
+ _record_sparse_plan_stats(model, q_ranges, k_ranges, types)
1169
+ return plan
1170
+
1171
+
1172
+ def _direct_base_forward(
1173
+ base,
1174
+ input_ids=None,
1175
+ visual_features=None,
1176
+ image_token_index=None,
1177
+ attention_mask=None,
1178
+ position_ids=None,
1179
+ past_key_values=None,
1180
+ inputs_embeds=None,
1181
+ use_cache=None,
1182
+ output_attentions=None,
1183
+ output_hidden_states=None,
1184
+ return_dict=None,
1185
+ ):
1186
+ mod = importlib.import_module(type(base).__module__)
1187
+ output_attentions = output_attentions if output_attentions is not None else base.config.output_attentions
1188
+ output_hidden_states = (
1189
+ output_hidden_states if output_hidden_states is not None else base.config.output_hidden_states
1190
+ )
1191
+ use_cache = use_cache if use_cache is not None else base.config.use_cache
1192
+
1193
+ if input_ids is not None and inputs_embeds is not None:
1194
+ raise ValueError("You cannot specify both decoder_input_ids and decoder_inputs_embeds at the same time")
1195
+ if input_ids is not None:
1196
+ batch_size, seq_length = input_ids.shape
1197
+ elif inputs_embeds is not None:
1198
+ batch_size, seq_length, _ = inputs_embeds.shape
1199
+ else:
1200
+ raise ValueError("You have to specify either decoder_input_ids or decoder_inputs_embeds")
1201
+
1202
+ past_key_values_length = 0
1203
+ use_legacy_cache = False
1204
+ if use_cache:
1205
+ Cache = getattr(mod, "Cache")
1206
+ DynamicCache = getattr(mod, "DynamicCache")
1207
+ use_legacy_cache = not isinstance(past_key_values, Cache)
1208
+ if use_legacy_cache:
1209
+ if past_key_values is None:
1210
+ past_key_values = DynamicCache()
1211
+ else:
1212
+ past_key_values = DynamicCache.from_legacy_cache(past_key_values)
1213
+ past_key_values_length = past_key_values.get_seq_length()
1214
+
1215
+ if position_ids is None:
1216
+ dev = input_ids.device if input_ids is not None else inputs_embeds.device
1217
+ position_ids = torch.arange(
1218
+ past_key_values_length,
1219
+ seq_length + past_key_values_length,
1220
+ dtype=torch.long,
1221
+ device=dev,
1222
+ ).unsqueeze(0).view(-1, seq_length)
1223
+ else:
1224
+ position_ids = position_ids.view(-1, seq_length).long()
1225
+
1226
+ if inputs_embeds is None:
1227
+ inputs_embeds = base.image_processing(input_ids, visual_features, image_token_index)
1228
+
1229
+ hidden_states = inputs_embeds
1230
+ all_hidden_states = () if output_hidden_states else None
1231
+ all_self_attns = () if output_attentions else None
1232
+ next_decoder_cache = None
1233
+
1234
+ for decoder_layer in base.layers:
1235
+ if output_hidden_states:
1236
+ all_hidden_states += (hidden_states,)
1237
+ layer_outputs = decoder_layer(
1238
+ hidden_states,
1239
+ attention_mask=attention_mask,
1240
+ position_ids=position_ids,
1241
+ past_key_value=past_key_values,
1242
+ output_attentions=output_attentions,
1243
+ use_cache=use_cache,
1244
+ )
1245
+ hidden_states = layer_outputs[0]
1246
+ if use_cache:
1247
+ next_decoder_cache = layer_outputs[2 if output_attentions else 1]
1248
+ if output_attentions:
1249
+ all_self_attns += (layer_outputs[1],)
1250
+
1251
+ hidden_states = base.norm(hidden_states)
1252
+ if output_hidden_states:
1253
+ all_hidden_states += (hidden_states,)
1254
+ next_cache = None
1255
+ if use_cache:
1256
+ next_cache = next_decoder_cache.to_legacy_cache() if use_legacy_cache else next_decoder_cache
1257
+ return SimpleNamespace(
1258
+ last_hidden_state=hidden_states,
1259
+ past_key_values=next_cache,
1260
+ hidden_states=all_hidden_states,
1261
+ attentions=all_self_attns,
1262
+ )
1263
+
1264
+
1265
+ def language_model_forward(model, **kwargs):
1266
+ """Forward through the text LM, bypassing official dense-mask prep for sparse plans."""
1267
+ lm = model.language_model
1268
+ return_logits = kwargs.pop("return_logits", True)
1269
+ logits_slice = kwargs.pop("logits_slice", None)
1270
+ attention_mask = kwargs.get("attention_mask")
1271
+ use_direct_sparse = (
1272
+ getattr(model, "_la_flash_requested_attn", ATTN_MODE) in {"magi", "la_flash"}
1273
+ and _is_magi_plan(attention_mask)
1274
+ )
1275
+ if not use_direct_sparse:
1276
+ return lm(**kwargs)
1277
+
1278
+ labels = kwargs.pop("labels", None)
1279
+ if labels is not None:
1280
+ raise NotImplementedError("labels are not supported in the direct sparse-plan decode forward")
1281
+ output_attentions = kwargs.get("output_attentions", None)
1282
+ output_hidden_states = kwargs.get("output_hidden_states", None)
1283
+ base_out = _direct_base_forward(lm.model, **kwargs)
1284
+ logits = None
1285
+ if return_logits:
1286
+ hidden_states = base_out.last_hidden_state
1287
+ if logits_slice is not None:
1288
+ hidden_states = hidden_states[:, logits_slice, :]
1289
+ logits = lm.lm_head(hidden_states).float()
1290
+ return SimpleNamespace(
1291
+ logits=logits,
1292
+ past_key_values=base_out.past_key_values,
1293
+ hidden_states=base_out.hidden_states if output_hidden_states else None,
1294
+ attentions=base_out.attentions if output_attentions else None,
1295
+ )
1296
+
1297
+
1298
+ _EagerCls = _SdpaCls = _LaFlashCls = _MagiCls = None
1299
+ def _attn_classes(mode=None):
1300
+ """Attention classes from the dynamic Qwen2 remote module.
1301
+
1302
+ The official Qwen2Model mask dispatcher only implements ``sdpa`` and
1303
+ single-row ``magi``. Eager, LA Flash, and batched Magi inference
1304
+ therefore swap the layer class while keeping the model's mask dispatcher
1305
+ pinned to ``sdpa``.
1306
+ """
1307
+ global _EagerCls, _SdpaCls, _LaFlashCls, _MagiCls
1308
+ mode = _normalize_attn_mode(mode) if mode is not None else None
1309
+ if _SdpaCls is None:
1310
+ mod = importlib.import_module(type(_model.language_model.model).__module__)
1311
+ _EagerCls = mod.Qwen2Attention
1312
+ _SdpaCls = mod.Qwen2SdpaAttention
1313
+ else:
1314
+ mod = importlib.import_module(type(_model.language_model.model).__module__)
1315
+ if (mode is None or mode == "la_flash") and _LaFlashCls is None:
1316
+ _LaFlashCls = build_la_flash_attention_class(mod)
1317
+ if (mode is None or mode == "magi") and _MagiCls is None:
1318
+ _MagiCls = build_batched_magi_attention_class(mod) if getattr(mod, "_MAGI_AVAILABLE", False) else None
1319
+ return _EagerCls, _SdpaCls, _LaFlashCls, _MagiCls
1320
+
1321
+ def _set_llm_mode(model, mode):
1322
+ """Swap every Qwen2 decoder layer's attention class.
1323
+
1324
+ Release backends keep ``Qwen2Model._attn_implementation='sdpa'`` so the
1325
+ official Qwen2 mask dispatcher stays available for dense-mask modes. The
1326
+ local ``la_flash`` and batched ``magi`` wrappers can also consume scheduler-built
1327
+ sparse plans directly, avoiding repeated per-layer dense mask conversion.
1328
+ """
1329
+ mode = _normalize_attn_mode(mode)
1330
+ eager, sdpa, la_flash, magi = _attn_classes(mode)
1331
+ impl = "sdpa"
1332
+ if mode == "sdpa":
1333
+ cls = sdpa
1334
+ elif mode == "eager":
1335
+ cls = eager
1336
+ elif mode == "la_flash":
1337
+ cls = la_flash
1338
+ elif mode == "magi":
1339
+ if magi is None:
1340
+ raise RuntimeError("MagiAttention is unavailable in the current Python environment.")
1341
+ cls = magi
1342
+ else:
1343
+ raise ValueError(f"unknown LLM attention mode: {mode}")
1344
+ llm = model.language_model.model
1345
+ for lyr in llm.layers:
1346
+ lyr.self_attn.__class__ = cls
1347
+ if mode == "magi":
1348
+ lyr.self_attn.softmax_scale = lyr.self_attn.head_dim ** -0.5
1349
+ llm._attn_implementation = impl
1350
+ llm.config._attn_implementation = llm._attn_implementation
1351
+ if hasattr(model.config, "text_config"):
1352
+ model.config.text_config._attn_implementation = llm._attn_implementation
1353
+ model.config._attn_implementation = llm._attn_implementation
1354
+ model._la_flash_requested_attn = mode
1355
+
1356
+ _st = _hp = None
1357
+ def _helpers():
1358
+ """The model's own sample_tokens / handle_pattern (the exact box decoders)."""
1359
+ global _st, _hp
1360
+ if _st is None:
1361
+ m = importlib.import_module(type(load()[2]).__module__)
1362
+ _st, _hp = m.sample_tokens, m.handle_pattern
1363
+ return _st, _hp
1364
+
1365
+
1366
+ _gu = None
1367
+ def _gen_utils():
1368
+ """The model's generate_utils module (apply_repetition_penalty / top_p_logits / top_k_logits /
1369
+ decode_bbox_avg / decode_ref / dists) -- the pieces sample_tokens_batched reuses verbatim."""
1370
+ global _gu
1371
+ if _gu is None:
1372
+ m = importlib.import_module(type(load()[2]).__module__)
1373
+ _gu = importlib.import_module(m.sample_tokens.__module__)
1374
+ return _gu
1375
+
1376
+
1377
+ def _env_float(name, default):
1378
+ val = os.environ.get(name)
1379
+ if val is None or val.strip() == "":
1380
+ return float(default)
1381
+ return float(val)
1382
+
1383
+
1384
+ def _coord_fallback_mode():
1385
+ mode = os.environ.get("LA_FLASH_COORD_FALLBACK_MODE", "legacy").strip().lower().replace("-", "_")
1386
+ aliases = {
1387
+ "": "legacy",
1388
+ "official": "legacy",
1389
+ "range": "legacy",
1390
+ "spread": "legacy",
1391
+ "none": "off",
1392
+ "disable": "off",
1393
+ "disabled": "off",
1394
+ "entropy_variance": "uncertainty",
1395
+ "entropy_var": "uncertainty",
1396
+ "ent_var": "uncertainty",
1397
+ "entropy_std": "uncertainty",
1398
+ }
1399
+ mode = aliases.get(mode, mode)
1400
+ if mode not in {"legacy", "uncertainty", "off"}:
1401
+ raise ValueError(
1402
+ "LA_FLASH_COORD_FALLBACK_MODE must be one of legacy, uncertainty, off"
1403
+ )
1404
+ return mode
1405
+
1406
+
1407
+ def _coord_uncertainty_threshold(coord_start_token_id, coord_end_token_id):
1408
+ """Return the coord uncertainty threshold in raw coord-token units.
1409
+
1410
+ Backward-compatible behavior:
1411
+ - LA_FLASH_COORD_UNCERTAINTY_THRESH > 1 is treated as raw coord-token RMSE.
1412
+ - LA_FLASH_COORD_UNCERTAINTY_THRESH <= 1 is treated as normalized by coord span.
1413
+ - LA_FLASH_COORD_UNCERTAINTY_NORM_THRESH is an explicit normalized override.
1414
+ """
1415
+ coord_span = max(float(coord_end_token_id - coord_start_token_id + 1), 1.0)
1416
+ norm_val = os.environ.get("LA_FLASH_COORD_UNCERTAINTY_NORM_THRESH")
1417
+ if norm_val is not None and norm_val.strip() != "":
1418
+ return float(norm_val) * coord_span
1419
+
1420
+ val = os.environ.get("LA_FLASH_COORD_UNCERTAINTY_THRESH")
1421
+ if val is None or val.strip() == "":
1422
+ return 20.0
1423
+ threshold = float(val)
1424
+ if 0.0 < threshold <= 1.0:
1425
+ return threshold * coord_span
1426
+ return threshold
1427
+
1428
+
1429
+ def _decode_bbox_with_uncertainty(logits, probs, token_ids, keep_k=4, generation_mode="hybrid"):
1430
+ """Decode an MTP box with configurable coord uncertainty fallback.
1431
+
1432
+ The default mode is the official LocateAnything rule. ``uncertainty`` keeps
1433
+ the same frame checks and top-k coord selection, but uses one scalar
1434
+ criterion per coordinate: the posterior RMSE of committing to the current
1435
+ MAP coordinate among valid coord candidates. This is the Bayes risk under
1436
+ squared coordinate error, so probabilities and token distances are folded
1437
+ into one threshold in coordinate-token units.
1438
+ """
1439
+ gu = _gen_utils()
1440
+ mode = _coord_fallback_mode()
1441
+ if mode == "legacy" or generation_mode != "hybrid":
1442
+ return gu.decode_bbox_avg(logits, probs, token_ids, keep_k=keep_k, generation_mode=generation_mode)
1443
+
1444
+ coord_start_token_id = token_ids["coord_start_token_id"]
1445
+ coord_end_token_id = token_ids["coord_end_token_id"]
1446
+ box_start_token_id = token_ids["box_start_token_id"]
1447
+ box_end_token_id = token_ids["box_end_token_id"]
1448
+ none_token_id = token_ids["none_token_id"]
1449
+ null_token_id = token_ids["null_token_id"]
1450
+ device = logits.device
1451
+
1452
+ box_type = gu.is_valid_box_frame(
1453
+ probs,
1454
+ token_ids,
1455
+ start_thresh=_env_float("LA_FLASH_COORD_BOX_START_THRESH", 0.7),
1456
+ end_thresh=_env_float("LA_FLASH_COORD_BOX_END_THRESH", 0.2),
1457
+ topk=keep_k,
1458
+ )
1459
+ if box_type == "empty_box":
1460
+ return torch.tensor([
1461
+ box_start_token_id,
1462
+ none_token_id,
1463
+ box_end_token_id,
1464
+ null_token_id,
1465
+ null_token_id,
1466
+ null_token_id,
1467
+ ], dtype=torch.long, device=device)
1468
+ if box_type == "illegal_box":
1469
+ return None
1470
+
1471
+ pos_probs, pos_ids = torch.topk(probs[1:5], k=keep_k, dim=-1)
1472
+ valid = (pos_ids >= coord_start_token_id) & (pos_ids <= coord_end_token_id)
1473
+ has_valid = valid.any(dim=-1)
1474
+ if not has_valid.all():
1475
+ return None
1476
+
1477
+ first_valid_idx = valid.long().argmax(dim=-1, keepdim=True)
1478
+ first_valid_ids = pos_ids.gather(-1, first_valid_idx).squeeze(-1)
1479
+ if mode == "off":
1480
+ final_coords = first_valid_ids
1481
+ else:
1482
+ valid_counts = valid.sum(dim=-1)
1483
+ valid_probs = torch.where(valid, pos_probs, torch.zeros_like(pos_probs))
1484
+ valid_mass = valid_probs.sum(dim=-1).clamp_min(1e-12)
1485
+ weights = valid_probs / valid_mass.unsqueeze(-1)
1486
+ coord_values = (pos_ids - coord_start_token_id).to(dtype=torch.float32)
1487
+ map_coord = (first_valid_ids - coord_start_token_id).to(dtype=torch.float32)
1488
+ uncertainty = (weights * (coord_values - map_coord.unsqueeze(-1)).pow(2)).sum(dim=-1).sqrt()
1489
+ is_abnormal = (
1490
+ (valid_counts > 1)
1491
+ & (uncertainty > _coord_uncertainty_threshold(coord_start_token_id, coord_end_token_id))
1492
+ )
1493
+ final_coords = torch.where(is_abnormal, torch.tensor(0, device=device), first_valid_ids)
1494
+
1495
+ start_t = torch.tensor([box_start_token_id], dtype=final_coords.dtype, device=device)
1496
+ end_t = torch.tensor([box_end_token_id], dtype=final_coords.dtype, device=device)
1497
+ return torch.cat([start_t, final_coords, end_t])
1498
+
1499
+
1500
+ def _apply_repetition_penalty_lowmem(logits, generated, repetition_penalty):
1501
+ """Apply the stock repetition penalty without allocating a [B, S, V] mask."""
1502
+ if repetition_penalty == 1.0:
1503
+ return logits
1504
+ _, _, vocab_size = logits.shape
1505
+ for row in range(logits.shape[0]):
1506
+ valid_tokens = generated[row].unique()
1507
+ valid_tokens = valid_tokens[(valid_tokens >= 0) & (valid_tokens < vocab_size)]
1508
+ if valid_tokens.numel() == 0:
1509
+ continue
1510
+ row_logits = logits[row, :, valid_tokens]
1511
+ logits[row, :, valid_tokens] = torch.where(
1512
+ row_logits > 0,
1513
+ row_logits / repetition_penalty,
1514
+ row_logits * repetition_penalty,
1515
+ )
1516
+ return logits
1517
+
1518
+
1519
+ def _finite_logit_bounds(dtype):
1520
+ finfo = torch.finfo(dtype)
1521
+ return finfo.min, finfo.max
1522
+
1523
+
1524
+ def _finite_logits(logits):
1525
+ if not logits.dtype.is_floating_point:
1526
+ logits = logits.float()
1527
+ min_val, max_val = _finite_logit_bounds(logits.dtype)
1528
+ return torch.nan_to_num(logits, nan=min_val, posinf=max_val, neginf=min_val)
1529
+
1530
+
1531
+ def _finite_logits_(logits):
1532
+ if not logits.dtype.is_floating_point:
1533
+ return logits.float()
1534
+ min_val, max_val = _finite_logit_bounds(logits.dtype)
1535
+ return logits.nan_to_num_(nan=min_val, posinf=max_val, neginf=min_val)
1536
+
1537
+
1538
+ def _top_p_logits_slice_(logits, top_p):
1539
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True)
1540
+ cumulative_probs = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1)
1541
+ sorted_indices_to_remove = cumulative_probs > top_p
1542
+ sorted_indices_to_remove[..., 1:] = sorted_indices_to_remove[..., :-1].clone()
1543
+ sorted_indices_to_remove[..., 0] = False
1544
+
1545
+ remove = torch.zeros_like(logits, dtype=torch.bool, device=logits.device)
1546
+ remove.scatter_(-1, sorted_indices, sorted_indices_to_remove)
1547
+ logits.masked_fill_(remove, torch.finfo(logits.dtype).min)
1548
+ return logits
1549
+
1550
+
1551
+ def _top_p_logits_(logits, top_p):
1552
+ """In-place nucleus filtering with bounded sort workspace.
1553
+
1554
+ The MTP sampler uses logits shaped ``[B, 6, V]``. Top-p is independent for
1555
+ each row and each future position, so filtering one position at a time keeps
1556
+ the expensive sorted-index workspace at ``[B, V]`` instead of ``[B, 6, V]``.
1557
+ """
1558
+ if logits.dim() == 3 and logits.shape[1] > 1:
1559
+ for pos in range(logits.shape[1]):
1560
+ _top_p_logits_slice_(logits[:, pos, :], top_p)
1561
+ return logits
1562
+ return _top_p_logits_slice_(logits, top_p)
1563
+
1564
+
1565
+ def _top_k_logits_(logits, top_k):
1566
+ """In-place top-k filtering mirroring generate_utils.top_k_logits."""
1567
+ top_k = min(int(top_k), logits.size(-1))
1568
+ threshold = torch.topk(logits, top_k)[0][..., -1, None]
1569
+ logits.masked_fill_(logits < threshold, torch.finfo(logits.dtype).min)
1570
+ return logits
1571
+
1572
+
1573
+ def _safe_probs(filtered_logits):
1574
+ """Softmax with CUDA-multinomial-safe cleanup and row-wise argmax fallback."""
1575
+ filtered_logits = _finite_logits(filtered_logits)
1576
+ probs = torch.softmax(filtered_logits, dim=-1, dtype=torch.float32)
1577
+ probs = torch.nan_to_num(probs, nan=0.0, posinf=0.0, neginf=0.0).clamp_min_(0.0)
1578
+ row_sum = probs.sum(dim=-1, keepdim=True)
1579
+ bad = (~torch.isfinite(row_sum)) | (row_sum <= 0)
1580
+ if bool(bad.any().item()):
1581
+ fallback = torch.zeros_like(probs)
1582
+ fallback.scatter_(-1, filtered_logits.argmax(dim=-1, keepdim=True), 1.0)
1583
+ probs = torch.where(bad, fallback, probs)
1584
+ row_sum = probs.sum(dim=-1, keepdim=True)
1585
+ return probs / row_sum.clamp_min(1.0e-20)
1586
+
1587
+
1588
+ def _sample_top_p_sorted_tokens(logits, top_p):
1589
+ """Sample from top-p filtered logits without scattering back to vocab order."""
1590
+ sorted_logits, sorted_indices = torch.sort(logits, descending=True, dim=-1)
1591
+ cumulative_probs = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1)
1592
+ remove = cumulative_probs > top_p
1593
+ remove[..., 1:] = remove[..., :-1].clone()
1594
+ remove[..., 0] = False
1595
+ sorted_logits.masked_fill_(remove, torch.finfo(sorted_logits.dtype).min)
1596
+ sorted_probs = _safe_probs(sorted_logits)
1597
+ sample_idx = sorted_probs.argmax(dim=-1)
1598
+ try:
1599
+ sample_idx = torch.distributions.Categorical(probs=sorted_probs).sample()
1600
+ except Exception:
1601
+ pass
1602
+ return sorted_indices.gather(-1, sample_idx.unsqueeze(-1)).squeeze(-1)
1603
+
1604
+
1605
+ @torch.no_grad()
1606
+ def sample_tokens_batched(logits, generated, token_ids, per_row_temp,
1607
+ repetition_penalty=1.0, top_p=None, top_k=None,
1608
+ keep_k_avg=4, generation_mode='fast'):
1609
+ """Batched fork of generate_utils.sample_tokens for the MTP window [B,6,V]. The logits pipeline
1610
+ (rep-penalty / per-row temperature / top_p / top_k / softmax / sample) is ROW-INDEPENDENT, so run
1611
+ it ONCE over the whole batch instead of B times on [1,6,V] (the per-row san defeats batching by
1612
+ slicing wlogits[b:b+1]). Only the variable-length box ASSEMBLY (decode_bbox_avg -> ragged shapes,
1613
+ where sample_tokens' final torch.stack throws) stays per-row, returned as a LIST.
1614
+
1615
+ Equivalence to per-row san: every pipeline op reduces on dim=-1 only (never crosses the row dim),
1616
+ so row b's processed logits/probs are bit-identical to slicing first -> greedy (per_row_temp==0,
1617
+ argmax branch, no RNG) is BIT-EXACT. Under sampling, one batched Categorical changes the global
1618
+ RNG consumption order vs B per-row draws -> box-size jitter (blessed; greedy is the exact gate).
1619
+ apply_repetition_penalty already loops per-row internally, so passing the full [B,M] `generated`
1620
+ is row-correct. keep_k_avg/generation_mode mirror sample_tokens' decode_bbox_avg call EXACTLY
1621
+ (note: the per-row san passes keep_k=5 but decode_bbox_avg reads keep_k_avg, default 4 -- so 5 is
1622
+ a no-op there; we replicate keep_k_avg=4). Returns (x0[B,6], boxes: list of B 1-D LongTensors)."""
1623
+ gu = _gen_utils()
1624
+ B, S, V = logits.shape # S = N_FUTURE = 6
1625
+ if repetition_penalty != 1.0:
1626
+ logits = _apply_repetition_penalty_lowmem(logits, generated, repetition_penalty)
1627
+ t = per_row_temp.to(dtype=logits.dtype).view(B, 1, 1)
1628
+ sample_rows = per_row_temp > 0
1629
+ if bool(sample_rows.all().item()):
1630
+ logits.div_(t.clamp(min=1e-8))
1631
+ elif bool(sample_rows.any().item()):
1632
+ idx = sample_rows.nonzero(as_tuple=True)[0]
1633
+ logits[idx].div_(t[idx].clamp(min=1e-8))
1634
+ logits = _finite_logits_(logits)
1635
+ if top_p is not None and top_p < 1:
1636
+ logits = _top_p_logits_(logits, top_p)
1637
+ if top_k is not None and top_k > 0:
1638
+ logits = _top_k_logits_(logits, top_k)
1639
+ probs = _safe_probs(logits)
1640
+ x0 = probs.argmax(dim=-1) # [B,6]; greedy rows are final here
1641
+ samp = per_row_temp > 0
1642
+ if bool(samp.any()): # sampling rows: ONE batched Categorical draw
1643
+ idx = samp.nonzero(as_tuple=True)[0]
1644
+ try:
1645
+ x0[idx] = gu.dists.Categorical(probs=probs[idx]).sample()
1646
+ except Exception:
1647
+ pass # keep argmax (matches san's except: probs.max)
1648
+ boxes = []
1649
+ fallback = torch.zeros(1, dtype=x0.dtype, device=x0.device)
1650
+ for b in range(B): # variable-length box assembly (per-row, exact)
1651
+ db = _decode_bbox_with_uncertainty(
1652
+ logits[b], probs[b], token_ids,
1653
+ keep_k=keep_k_avg, generation_mode=generation_mode)
1654
+ if db is not None:
1655
+ boxes.append(db)
1656
+ else:
1657
+ ref = gu.decode_ref(logits[b], probs[b], token_ids)
1658
+ if ref is None:
1659
+ boxes.append(fallback)
1660
+ elif torch.is_tensor(ref):
1661
+ boxes.append(ref.to(dtype=x0.dtype, device=x0.device))
1662
+ else:
1663
+ boxes.append(torch.tensor(ref, dtype=x0.dtype, device=x0.device))
1664
+ return x0, boxes
1665
+
1666
+
1667
+ @torch.no_grad()
1668
+ def sample_next_tokens_batched(logits, generated, per_row_temp,
1669
+ repetition_penalty=1.0, top_p=None, top_k=None):
1670
+ """Batched one-token sampler for AR repair rows.
1671
+
1672
+ This mirrors the row-independent part of ``sample_tokens`` for logits shaped
1673
+ ``[B,1,V]``. It intentionally does not run bbox/ref assembly because AR mode
1674
+ only needs the next token before the state machine classifies it.
1675
+ """
1676
+ gu = _gen_utils()
1677
+ if logits.dim() != 3 or logits.shape[1] != 1:
1678
+ raise ValueError(f"AR batched sampler expects logits [B,1,V], got {tuple(logits.shape)}")
1679
+ B = int(logits.shape[0])
1680
+ if repetition_penalty != 1.0:
1681
+ logits = _apply_repetition_penalty_lowmem(logits, generated, repetition_penalty)
1682
+ t = per_row_temp.to(dtype=logits.dtype).view(B, 1, 1)
1683
+ sample_rows = per_row_temp > 0
1684
+ if bool(sample_rows.all().item()):
1685
+ logits.div_(t.clamp(min=1e-8))
1686
+ elif bool(sample_rows.any().item()):
1687
+ idx = sample_rows.nonzero(as_tuple=True)[0]
1688
+ logits[idx].div_(t[idx].clamp(min=1e-8))
1689
+ logits = _finite_logits_(logits)
1690
+ sorted_top_p = os.environ.get("AR_SORTED_TOPP", "0") == "1"
1691
+ default_top_p = sorted_top_p and top_p is not None and top_p < 1 and (top_k is None or top_k <= 0)
1692
+ if default_top_p and bool(sample_rows.all().item()):
1693
+ return _sample_top_p_sorted_tokens(logits, top_p)
1694
+ if top_p is not None and top_p < 1:
1695
+ logits = _top_p_logits_(logits, top_p)
1696
+ if top_k is not None and top_k > 0:
1697
+ logits = _top_k_logits_(logits, top_k)
1698
+ probs = _safe_probs(logits)
1699
+ x0 = probs.argmax(dim=-1)
1700
+ if bool(sample_rows.any().item()):
1701
+ # Keep row-ordered sampling as the release default. A single batched
1702
+ # Categorical is faster, but it consumes RNG differently from stock AR
1703
+ # repair and can alter default-temperature termination behavior.
1704
+ for row in sample_rows.nonzero(as_tuple=True)[0].tolist():
1705
+ try:
1706
+ x0[row : row + 1] = gu.dists.Categorical(probs=probs[row : row + 1]).sample()
1707
+ except Exception:
1708
+ pass
1709
+ return x0
1710
+
1711
+
1712
+ def load_pil(p):
1713
+ from PIL import Image
1714
+ im = Image.open(p).convert("RGB"); w, h = im.size
1715
+ if max(w, h) > MAX_DIM:
1716
+ s = MAX_DIM / max(w, h); im = im.resize((max(1, round(w*s)), max(1, round(h*s))), Image.LANCZOS)
1717
+ return im
1718
+
1719
+ def _preproc_one(im):
1720
+ """CPU-side processor for one image -> (pixel_values[bf16], grid[int32]). Split out of
1721
+ _encode_image so _encode_images can batch the GPU encode while preprocessing stays per-image."""
1722
+ tok, proc, model = load()
1723
+ msg = [{"role": "user", "content": [{"type": "image", "image": im}, {"type": "text", "text": "x"}]}]
1724
+ text = proc.py_apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
1725
+ imgs, vids = proc.process_vision_info(msg)
1726
+ inp = proc(text=[text], images=imgs, videos=vids, return_tensors="pt").to(DEV)
1727
+ grid = inp.get("image_grid_hws")
1728
+ if isinstance(grid, np.ndarray): grid = torch.from_numpy(grid).to(DEV, dtype=torch.int32)
1729
+ return inp["pixel_values"].to(DT), grid
1730
+
1731
+
1732
+ def _vision_is_flash():
1733
+ """True iff MoonViT will actually run flash_attn_varlen (so cross-image packing is
1734
+ block-diagonal = exact AND a win). If the vision blocks are on sdpa/eager, OR the flash
1735
+ wheel is absent (multihead_attention falls back to the dense-mask sdpa path), packing is
1736
+ O(S^2) N^2 -> caller must stay per-image."""
1737
+ vm = load()[2].vision_model
1738
+ mod = importlib.import_module(type(vm).__module__)
1739
+ if getattr(mod, "flash_attn_varlen_func", None) is None:
1740
+ return False
1741
+ try:
1742
+ return vm.encoder.blocks[0].attn_implementation == "flash_attention_2"
1743
+ except Exception:
1744
+ return False
1745
+
1746
+
1747
+ @torch.no_grad()
1748
+ def _encode_images(ims):
1749
+ """N images -> list of [n_img_tokens, C] mlp1-projected visual_features, one per image
1750
+ (row-order). Drop-in for [_encode_image(im) for im in ims].
1751
+
1752
+ With flash present (_vision_is_flash) and N>1, packs images into
1753
+ extract_feature micro-batches: MoonViT's varlen cu_seqlens path is
1754
+ block-diagonal by image. Without flash, the dense SDPA fallback would scale
1755
+ with the packed total sequence length, so this function falls back to
1756
+ per-image encode. MTP_BATCH_VISION=0 also forces per-image encode."""
1757
+ tok, proc, model = load()
1758
+ pvs, grids = [], []
1759
+ for im in ims:
1760
+ pv, g = _preproc_one(im)
1761
+ pvs.append(pv); grids.append(g)
1762
+ if BATCH_VISION and len(ims) > 1 and _vision_is_flash():
1763
+ if VISION_ENCODE_BATCH_SIZE <= 0 or VISION_ENCODE_BATCH_SIZE >= len(ims):
1764
+ vit_list = model.extract_feature(torch.cat(pvs, dim=0), torch.cat(grids, dim=0))
1765
+ else:
1766
+ vit_list = []
1767
+ for start in range(0, len(ims), VISION_ENCODE_BATCH_SIZE):
1768
+ end = min(start + VISION_ENCODE_BATCH_SIZE, len(ims))
1769
+ vit_list.extend(
1770
+ model.extract_feature(
1771
+ torch.cat(pvs[start:end], dim=0),
1772
+ torch.cat(grids[start:end], dim=0),
1773
+ )
1774
+ )
1775
+ return [model.mlp1(v) for v in vit_list] # one [P_i, C] per image (patch_merger split)
1776
+ return [model.mlp1(torch.cat(model.extract_feature(pv, g), dim=0))
1777
+ for pv, g in zip(pvs, grids)] # per-image (flash absent / N==1 / forced off)
1778
+
1779
+
1780
+ @torch.no_grad()
1781
+ def _encode_image(im):
1782
+ """Single-image convenience wrapper (single-image callers); = _encode_images([im])[0]
1783
+ (takes the per-image path inside _encode_images, so bit-identical to the original)."""
1784
+ return _encode_images([im])[0]
1785
+
1786
+ @torch.no_grad()
1787
+ def _tokenize(im, query):
1788
+ """1-D prompt token ids for (image, query). Uses the model's own chat template."""
1789
+ tok, proc, model = load()
1790
+ msg = [{"role": "user", "content": [{"type": "image", "image": im},
1791
+ {"type": "text", "text": _PROMPT + query + "."}]}]
1792
+ text = proc.py_apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
1793
+ imgs, vids = proc.process_vision_info(msg)
1794
+ return proc(text=[text], images=imgs, videos=vids, return_tensors="pt").to(DEV)["input_ids"][0]
1795
+
1796
+
1797
+ @torch.no_grad()
1798
+ def _tokenize_cached_image(query, image_token_count, im=None):
1799
+ """Tokenize a prompt when the image token count is already known.
1800
+
1801
+ This keeps the processor's chat template, but directly expands ``<image-1>``
1802
+ from the cached visual feature length. It avoids re-running the CPU image
1803
+ processor for every category prompt that shares the same image.
1804
+ """
1805
+ tok, proc, model = load()
1806
+ msg = [{"role": "user", "content": [{"type": "image", "image": im},
1807
+ {"type": "text", "text": _PROMPT + query + "."}]}]
1808
+ text = proc.py_apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
1809
+ placeholder = f"<{getattr(proc, 'image_placeholder', 'image')}-1>"
1810
+ image_token = getattr(proc, "image_token", "<IMG_CONTEXT>")
1811
+ image_start = getattr(proc, "image_start_token", "<img>")
1812
+ image_end = getattr(proc, "image_end_token", "</img>")
1813
+ replacement = f"<image 1>{image_start}{image_token * int(image_token_count)}{image_end}"
1814
+ if placeholder not in text:
1815
+ raise ValueError(f"cached image placeholder {placeholder!r} was not found in chat template")
1816
+ text = text.replace(placeholder, replacement, 1)
1817
+ return tok([text], return_tensors="pt").to(DEV)["input_ids"][0]
1818
+
1819
+
1820
+ def _proc_full(im, query):
1821
+ """Full processor dict (input_ids, attention_mask, pixel_values, image_grid_hws) —
1822
+ used by the bench to drive the STOCK generate for the equivalence check."""
1823
+ tok, proc, model = load()
1824
+ msg = [{"role": "user", "content": [{"type": "image", "image": im},
1825
+ {"type": "text", "text": _PROMPT + query + "."}]}]
1826
+ text = proc.py_apply_chat_template(msg, tokenize=False, add_generation_prompt=True)
1827
+ imgs, vids = proc.process_vision_info(msg)
1828
+ inp = proc(text=[text], images=imgs, videos=vids, return_tensors="pt").to(DEV)
1829
+ grid = inp.get("image_grid_hws")
1830
+ if isinstance(grid, np.ndarray): grid = torch.from_numpy(grid).to(DEV, dtype=torch.int32)
1831
+ inp["image_grid_hws"] = grid
1832
+ return inp
1833
+
1834
+ def _pad_generated(prompt_ids, gen_ids, img_tok, dev):
1835
+ """Per-row [prompt + accepted] left-padded with the image token (already in every
1836
+ prompt -> .unique() unchanged -> repetition penalty identical to single-run)."""
1837
+ rows = [list(prompt_ids[b].tolist()) + gen_ids[b] for b in range(len(prompt_ids))]
1838
+ M = max(len(r) for r in rows)
1839
+ out = torch.full((len(rows), M), img_tok, dtype=torch.long, device=dev)
1840
+ for b, r in enumerate(rows):
1841
+ out[b, M - len(r):] = torch.tensor(r, dtype=torch.long, device=dev)
1842
+ return out
kernel_utils/README.md ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # LA Flash Utils
2
+
3
+ This folder contains the sparse attention utilities used by
4
+ `LA_FLASH_ATTN=la_flash`. The release path is implemented with
5
+ FlashAttention varlen over LocateAnything range plans. It does not include or
6
+ build a local C++/CUDA extension.
7
+
8
+ ## Features
9
+
10
+ - Supports batched LocateAnything hybrid MTP inference on A100, RTX 4090, and H100.
11
+ - Consumes Magi-style `q_ranges`, `k_ranges`, `segment_offsets`, and
12
+ `attn_type_map` plans generated by `batch_utils.hybrid_runtime`.
13
+ - Uses FlashAttention varlen for packed causal/full plans.
14
+ - Packs LocateAnything MTP full-window key segments before calling
15
+ FlashAttention, avoiding dense `[B,H,Q,K]` masks.
16
+ - Supports log-sum-exp merging for compatible non-packed multi-segment plans.
17
+
18
+ ## Attention Types
19
+
20
+ The release path intentionally supports only FlashAttention-compatible plan
21
+ types:
22
+
23
+ | Value | Meaning |
24
+ | --- | --- |
25
+ | `0` | Full attention over the listed key segment or packed key segments. |
26
+ | `1` | Bottom-right causal attention. |
27
+
28
+ The old local CUDA-only encodings (`100 + block_size` and `10000 + ...`) were
29
+ removed from the release.
30
+
31
+ ## Runtime Knobs
32
+
33
+ | Variable | Default | Meaning |
34
+ | --- | --- | --- |
35
+ | `LA_FLASH_ATTN` | `sdpa` | Set to `la_flash` to enable this backend through `batch_utils`. |
36
+ | `LA_FLASH_FASTPATH` | `auto` | Use FlashAttention varlen for packed simple plans. |
37
+ | `LA_FLASH_SEGMENT_FASTPATH` | `auto` | Use FlashAttention varlen for multi-segment sparse plans. Full segments are packed first; other compatible segments use LSE merging. |
38
+ | `LA_FLASH_PLAN_STATS` | `0` | Record sparse plan statistics in inference summaries. |
39
+
40
+ ## Notes
41
+
42
+ Dense prefill and stock worker-style generation should keep
43
+ `LA_FLASH_DENSE_BACKEND=sdpa`; LA Flash is used for sparse range plans
44
+ produced by `batch_utils`.
45
+
46
+ This package is for inference and evaluation. Training remains on the
47
+ MagiAttention backend; the batched sparse-plan decode runtime does not support
48
+ the `labels` training path.
49
+
50
+ ## Source Layout
51
+
52
+ - `range_attention.py`: FlashAttention varlen dispatch, sparse KV packing, LSE
53
+ merge fallback, and availability checks.
kernel_utils/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """FlashAttention sparse range utilities for LocateAnything batch inference."""
2
+
3
+ from .range_attention import range_attention, is_available
4
+
5
+ __all__ = ["range_attention", "is_available"]
kernel_utils/range_attention.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sparse LocateAnything attention implemented with FlashAttention varlen.
2
+
3
+ The public API accepts flattened query/key/value tensors:
4
+
5
+ q: [total_q, num_q_heads, head_dim]
6
+ k: [total_k, num_kv_heads, head_dim]
7
+ v: [total_k, num_kv_heads, head_dim]
8
+
9
+ and a Magi-style range plan:
10
+
11
+ q_ranges: [num_ranges, 2]
12
+ k_ranges: [num_key_segments, 2]
13
+ segment_offsets: [num_query_groups + 1]
14
+ attn_type_map:
15
+ 0 = full attention over the listed key segment(s)
16
+ 1 = bottom-right causal attention
17
+
18
+ For LocateAnything hybrid MTP decode, batch_utils represents the window as a
19
+ causal prefix plus full-attention sparse window segments. This module packs
20
+ those visible KV segments and calls FlashAttention varlen, avoiding dense masks.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import os
25
+ from typing import Optional
26
+
27
+ import torch
28
+
29
+
30
+ _FLASH_ATTN_VARLEN = None
31
+ _FLASH_ATTN_ERROR: Optional[BaseException] = None
32
+
33
+
34
+ def _env_enabled(name: str, default: str = "auto") -> bool:
35
+ value = os.environ.get(name, default).strip().lower()
36
+ return value in {"", "auto", "1", "on", "true", "yes", "force"}
37
+
38
+
39
+ def is_available() -> bool:
40
+ try:
41
+ _load_flash_attn_varlen()
42
+ return True
43
+ except Exception:
44
+ return False
45
+
46
+
47
+ def _flash_fastpath_enabled() -> bool:
48
+ return _env_enabled("LA_FLASH_FASTPATH", "auto")
49
+
50
+
51
+ def _flash_segment_fastpath_enabled() -> bool:
52
+ return _env_enabled("LA_FLASH_SEGMENT_FASTPATH", "auto")
53
+
54
+
55
+ def _load_flash_attn_varlen():
56
+ global _FLASH_ATTN_VARLEN, _FLASH_ATTN_ERROR
57
+ if _FLASH_ATTN_VARLEN is not None:
58
+ return _FLASH_ATTN_VARLEN
59
+ if _FLASH_ATTN_ERROR is not None:
60
+ raise _FLASH_ATTN_ERROR
61
+ try:
62
+ from flash_attn import flash_attn_varlen_func
63
+
64
+ _FLASH_ATTN_VARLEN = flash_attn_varlen_func
65
+ return _FLASH_ATTN_VARLEN
66
+ except BaseException as exc:
67
+ _FLASH_ATTN_ERROR = exc
68
+ raise
69
+
70
+
71
+ def _coalesce_query_groups(q_ranges, k_ranges, attn_type_map):
72
+ """Group consecutive entries that share the same query span and mask type."""
73
+ if q_ranges.numel() == 0:
74
+ segment_offsets = torch.zeros((1,), dtype=torch.int32, device=q_ranges.device)
75
+ return q_ranges, k_ranges, segment_offsets, attn_type_map, 0, 0
76
+
77
+ q_cpu = q_ranges.detach().to(device="cpu", dtype=torch.int32).contiguous()
78
+ t_cpu = attn_type_map.detach().to(device="cpu", dtype=torch.int32).contiguous()
79
+ grouped_q = []
80
+ grouped_t = []
81
+ offsets = [0]
82
+ max_q_len = 0
83
+ last_q = None
84
+ last_t = None
85
+ for idx, (qr, attn_type) in enumerate(zip(q_cpu.tolist(), t_cpu.tolist())):
86
+ key = (int(qr[0]), int(qr[1]))
87
+ attn_type = int(attn_type)
88
+ if attn_type not in (0, 1):
89
+ raise RuntimeError(
90
+ "LA Flash path only supports attn_type 0/1. "
91
+ f"Got attn_type={attn_type}; regenerate the plan without the old local kernel encodings."
92
+ )
93
+ if last_q is None:
94
+ grouped_q.append([key[0], key[1]])
95
+ grouped_t.append(attn_type)
96
+ max_q_len = max(max_q_len, key[1] - key[0])
97
+ last_q = key
98
+ last_t = attn_type
99
+ continue
100
+ if key == last_q and attn_type == last_t:
101
+ continue
102
+ offsets.append(idx)
103
+ grouped_q.append([key[0], key[1]])
104
+ grouped_t.append(attn_type)
105
+ max_q_len = max(max_q_len, key[1] - key[0])
106
+ last_q = key
107
+ last_t = attn_type
108
+ offsets.append(int(q_ranges.shape[0]))
109
+
110
+ k_cpu = k_ranges.detach().to(device="cpu", dtype=torch.int32).contiguous()
111
+ max_k_len = max((int(end) - int(start) for start, end in k_cpu.tolist()), default=0)
112
+
113
+ return (
114
+ torch.tensor(grouped_q, dtype=torch.int32, device=q_ranges.device).contiguous(),
115
+ k_ranges,
116
+ torch.tensor(offsets, dtype=torch.int32, device=q_ranges.device).contiguous(),
117
+ torch.tensor(grouped_t, dtype=torch.int32, device=q_ranges.device).contiguous(),
118
+ int(max_q_len),
119
+ int(max_k_len),
120
+ )
121
+
122
+
123
+ def _flash_lse_to_tq_h(lse, total_q, q_lengths=None):
124
+ if lse is None:
125
+ return None
126
+ if lse.dim() != 2:
127
+ if lse.dim() == 3 and q_lengths is not None and lse.shape[0] == len(q_lengths):
128
+ chunks = []
129
+ for idx, q_len in enumerate(q_lengths):
130
+ q_len = int(q_len)
131
+ if lse.shape[1] == 0 or q_len > lse.shape[2]:
132
+ return None
133
+ chunks.append(lse[idx, :, :q_len].transpose(0, 1).contiguous())
134
+ merged = torch.cat(chunks, dim=0).float()
135
+ return merged if merged.shape[0] == total_q else None
136
+ return None
137
+ if lse.shape[0] == total_q:
138
+ return lse.float()
139
+ if lse.shape[1] == total_q:
140
+ return lse.transpose(0, 1).contiguous().float()
141
+ return None
142
+
143
+
144
+ def _make_cu_seqlens(lengths, device):
145
+ return torch.tensor([0] + list(torch.tensor(lengths).cumsum(0).tolist()), device=device, dtype=torch.int32)
146
+
147
+
148
+ def _try_flash_segment_merge(
149
+ q,
150
+ k,
151
+ v,
152
+ k_ranges,
153
+ segment_offsets,
154
+ group_q_ranges,
155
+ group_attn_type_map,
156
+ softmax_scale,
157
+ ):
158
+ if not _flash_segment_fastpath_enabled():
159
+ return None
160
+ if q.dtype not in (torch.float16, torch.bfloat16) or k.dtype != q.dtype or v.dtype != q.dtype:
161
+ return None
162
+ if group_q_ranges is None or segment_offsets is None or group_attn_type_map is None:
163
+ return None
164
+
165
+ flash_attn_varlen = _load_flash_attn_varlen()
166
+ gq_cpu = group_q_ranges.detach().to(device="cpu", dtype=torch.int32).contiguous()
167
+ kr_cpu = k_ranges.detach().to(device="cpu", dtype=torch.int32).contiguous()
168
+ seg_cpu = segment_offsets.detach().to(device="cpu", dtype=torch.int32).contiguous()
169
+ type_cpu = group_attn_type_map.detach().to(device="cpu", dtype=torch.int32).contiguous()
170
+
171
+ groups = []
172
+ max_segments = 0
173
+ for group_idx, (q_start, q_end) in enumerate(gq_cpu.tolist()):
174
+ attn_type = int(type_cpu[group_idx].item())
175
+ if attn_type not in (0, 1):
176
+ return None
177
+ seg_start = int(seg_cpu[group_idx].item())
178
+ seg_end = int(seg_cpu[group_idx + 1].item())
179
+ if seg_end <= seg_start or q_end <= q_start:
180
+ return None
181
+ segments = kr_cpu[seg_start:seg_end].tolist()
182
+ max_segments = max(max_segments, len(segments))
183
+ groups.append((int(q_start), int(q_end), attn_type, [(int(a), int(b)) for a, b in segments]))
184
+
185
+ if not groups or max_segments == 0:
186
+ return None
187
+
188
+ can_pack_full_groups = all(attn_type == 0 or len(segments) == 1 for _, _, attn_type, segments in groups)
189
+ if can_pack_full_groups:
190
+ merged = torch.empty((q.shape[0], q.shape[1], q.shape[2]), device=q.device, dtype=q.dtype)
191
+ covered = torch.zeros((q.shape[0],), device=q.device, dtype=torch.bool)
192
+ for attn_type in (0, 1):
193
+ q_slices = []
194
+ k_slices = []
195
+ v_slices = []
196
+ q_lengths = []
197
+ k_lengths = []
198
+ targets = []
199
+ for q_start, q_end, group_type, segments in groups:
200
+ if group_type != attn_type:
201
+ continue
202
+ q_slices.append(q[q_start:q_end])
203
+ if attn_type == 0 and len(segments) > 1:
204
+ k_slices.append(torch.cat([k[start:end] for start, end in segments], dim=0))
205
+ v_slices.append(torch.cat([v[start:end] for start, end in segments], dim=0))
206
+ k_lengths.append(sum(end - start for start, end in segments))
207
+ else:
208
+ k_start, k_end = segments[0]
209
+ k_slices.append(k[k_start:k_end])
210
+ v_slices.append(v[k_start:k_end])
211
+ k_lengths.append(k_end - k_start)
212
+ q_lengths.append(q_end - q_start)
213
+ targets.append((q_start, q_end))
214
+ if not q_slices:
215
+ continue
216
+
217
+ out_pass = flash_attn_varlen(
218
+ torch.cat(q_slices, dim=0).contiguous(),
219
+ torch.cat(k_slices, dim=0).contiguous(),
220
+ torch.cat(v_slices, dim=0).contiguous(),
221
+ _make_cu_seqlens(q_lengths, q.device),
222
+ _make_cu_seqlens(k_lengths, q.device),
223
+ int(max(q_lengths)),
224
+ int(max(k_lengths)),
225
+ dropout_p=0.0,
226
+ softmax_scale=float(softmax_scale),
227
+ causal=bool(attn_type == 1),
228
+ )
229
+ if isinstance(out_pass, tuple):
230
+ out_pass = out_pass[0]
231
+
232
+ cursor = 0
233
+ for q_start, q_end in targets:
234
+ q_len = q_end - q_start
235
+ merged[q_start:q_end] = out_pass[cursor:cursor + q_len]
236
+ covered[q_start:q_end] = True
237
+ cursor += q_len
238
+
239
+ if bool(covered.all().item()):
240
+ return merged
241
+
242
+ merged = torch.zeros((q.shape[0], q.shape[1], q.shape[2]), device=q.device, dtype=torch.float32)
243
+ merged_lse = torch.full((q.shape[0], q.shape[1]), -float("inf"), device=q.device, dtype=torch.float32)
244
+ covered = torch.zeros((q.shape[0],), device=q.device, dtype=torch.bool)
245
+
246
+ for segment_idx in range(max_segments):
247
+ for attn_type in (0, 1):
248
+ q_slices = []
249
+ k_slices = []
250
+ v_slices = []
251
+ q_lengths = []
252
+ k_lengths = []
253
+ targets = []
254
+ for q_start, q_end, group_type, segments in groups:
255
+ if group_type != attn_type or segment_idx >= len(segments):
256
+ continue
257
+ k_start, k_end = segments[segment_idx]
258
+ if k_end <= k_start:
259
+ continue
260
+ q_slices.append(q[q_start:q_end])
261
+ k_slices.append(k[k_start:k_end])
262
+ v_slices.append(v[k_start:k_end])
263
+ q_lengths.append(q_end - q_start)
264
+ k_lengths.append(k_end - k_start)
265
+ targets.append((q_start, q_end))
266
+ if not q_slices:
267
+ continue
268
+
269
+ result = flash_attn_varlen(
270
+ torch.cat(q_slices, dim=0).contiguous(),
271
+ torch.cat(k_slices, dim=0).contiguous(),
272
+ torch.cat(v_slices, dim=0).contiguous(),
273
+ _make_cu_seqlens(q_lengths, q.device),
274
+ _make_cu_seqlens(k_lengths, q.device),
275
+ int(max(q_lengths)),
276
+ int(max(k_lengths)),
277
+ dropout_p=0.0,
278
+ softmax_scale=float(softmax_scale),
279
+ causal=bool(attn_type == 1),
280
+ return_attn_probs=True,
281
+ )
282
+ if not isinstance(result, tuple) or len(result) < 2:
283
+ return None
284
+ out_pass = result[0]
285
+ lse_pass = _flash_lse_to_tq_h(result[1], out_pass.shape[0], q_lengths)
286
+ if lse_pass is None:
287
+ return None
288
+
289
+ cursor = 0
290
+ for q_start, q_end in targets:
291
+ q_len = q_end - q_start
292
+ out_seg = out_pass[cursor:cursor + q_len].float()
293
+ lse_seg = lse_pass[cursor:cursor + q_len]
294
+ old_lse = merged_lse[q_start:q_end]
295
+ new_lse = torch.maximum(old_lse, lse_seg)
296
+ old_w = torch.exp(old_lse - new_lse)
297
+ seg_w = torch.exp(lse_seg - new_lse)
298
+ denom = (old_w + seg_w).clamp_min(1e-20)
299
+ merged[q_start:q_end] = (
300
+ merged[q_start:q_end] * old_w.unsqueeze(-1)
301
+ + out_seg * seg_w.unsqueeze(-1)
302
+ ) / denom.unsqueeze(-1)
303
+ merged_lse[q_start:q_end] = new_lse + torch.log(denom)
304
+ covered[q_start:q_end] = True
305
+ cursor += q_len
306
+
307
+ if not bool(covered.all().item()):
308
+ return None
309
+ return merged.to(dtype=q.dtype)
310
+
311
+
312
+ def range_attention(
313
+ q,
314
+ k,
315
+ v,
316
+ q_ranges,
317
+ k_ranges,
318
+ attn_type_map,
319
+ softmax_scale: float,
320
+ *,
321
+ segment_offsets=None,
322
+ group_q_ranges=None,
323
+ group_attn_type_map=None,
324
+ max_q_len=None,
325
+ max_k_len=None,
326
+ flash_cu_seqlens_q=None,
327
+ flash_cu_seqlens_k=None,
328
+ flash_causal=None,
329
+ disjoint_q_ranges=None,
330
+ ):
331
+ """Run sparse range attention through FlashAttention varlen."""
332
+ del disjoint_q_ranges
333
+ if not q.is_cuda:
334
+ raise RuntimeError("LA Flash range_attention requires CUDA tensors")
335
+ if segment_offsets is None or group_q_ranges is None or group_attn_type_map is None:
336
+ (
337
+ group_q_ranges,
338
+ k_ranges,
339
+ segment_offsets,
340
+ group_attn_type_map,
341
+ computed_max_q_len,
342
+ computed_max_k_len,
343
+ ) = _coalesce_query_groups(q_ranges, k_ranges, attn_type_map)
344
+ if max_q_len is None:
345
+ max_q_len = computed_max_q_len
346
+ if max_k_len is None:
347
+ max_k_len = computed_max_k_len
348
+ elif max_q_len is None:
349
+ lengths = (group_q_ranges[:, 1] - group_q_ranges[:, 0]).detach().to(device="cpu")
350
+ max_q_len = int(lengths.max().item()) if lengths.numel() else 0
351
+ if max_k_len is None:
352
+ k_lengths = (k_ranges[:, 1] - k_ranges[:, 0]).detach().to(device="cpu")
353
+ max_k_len = int(k_lengths.max().item()) if k_lengths.numel() else 0
354
+
355
+ if (
356
+ flash_cu_seqlens_q is not None
357
+ and flash_cu_seqlens_k is not None
358
+ and flash_causal is not None
359
+ and _flash_fastpath_enabled()
360
+ and q.dtype in (torch.float16, torch.bfloat16)
361
+ and k.dtype == q.dtype
362
+ and v.dtype == q.dtype
363
+ ):
364
+ flash_attn_varlen = _load_flash_attn_varlen()
365
+ return flash_attn_varlen(
366
+ q.contiguous(),
367
+ k.contiguous(),
368
+ v.contiguous(),
369
+ flash_cu_seqlens_q.contiguous().to(device=q.device, dtype=torch.int32),
370
+ flash_cu_seqlens_k.contiguous().to(device=q.device, dtype=torch.int32),
371
+ int(max_q_len),
372
+ int(max_k_len),
373
+ dropout_p=0.0,
374
+ softmax_scale=float(softmax_scale),
375
+ causal=bool(flash_causal),
376
+ )
377
+
378
+ segment_out = _try_flash_segment_merge(
379
+ q,
380
+ k,
381
+ v,
382
+ k_ranges,
383
+ segment_offsets,
384
+ group_q_ranges,
385
+ group_attn_type_map,
386
+ softmax_scale,
387
+ )
388
+ if segment_out is not None:
389
+ return segment_out
390
+
391
+ raise RuntimeError(
392
+ "LA Flash could not express this range plan with FlashAttention varlen. "
393
+ "Only attn_type 0/1 range plans are supported in the release path."
394
+ )
pyproject.toml ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "locateanything-la-flash"
7
+ version = "0.1.0"
8
+ description = "LocateAnything batch utils with LA Flash inference backend."
9
+ requires-python = ">=3.10"
10
+ dependencies = [
11
+ "numpy",
12
+ "pillow",
13
+ "torch",
14
+ "transformers",
15
+ ]
16
+
17
+ [tool.setuptools]
18
+ packages = ["batch_utils", "kernel_utils"]