handwoven8588 commited on
Commit
8187d6a
·
verified ·
1 Parent(s): e361c6f

Three-tier attention: torch varlen, flash_attn, eager (inline mask)

Browse files
Files changed (2) hide show
  1. README.md +88 -41
  2. modeling_hf_nomic_bert.py +202 -46
README.md CHANGED
@@ -16,33 +16,57 @@ language:
16
  # CodeRankEmbed-flash-attn
17
 
18
  A **bf16 quantization of [`nomic-ai/CodeRankEmbed`](https://huggingface.co/nomic-ai/CodeRankEmbed)**
19
- with **flash-attention built into a custom `modeling_hf_nomic_bert.py` shipped in this repo.** It is
20
- not a finetune — the weights are the original CodeRankEmbed weights cast to bf16 (no further
21
- training) — and attention runs via `flash_attn` varlen instead of the original eager `O(seq²)` path.
 
 
22
 
23
  ## Why
24
 
25
  `nomic-ai/CodeRankEmbed` loads through `trust_remote_code`, and its attention path is **eager
26
  only** — activation memory grows as `batch × heads × seq²`, which OOMs at large batches even though
27
- the model is only 137M params. `flash_attn`'s varlen path computes the same attention in `O(N)`
28
- memory by packing unpadded sequences, so the large batches that OOM the eager path run comfortably —
29
- with **parity embeddings** (no quality change). This repo ships that flash path in the modeling file
30
- itself, so no runtime patching or post-load hooks are needed.
 
 
 
 
 
 
 
 
31
 
32
  ## Behavior
33
 
34
- - **Loads bf16 by default.** `flash_attn` requires half precision and the model runs bf16 in any
35
- real serving setup, so the weights are stored bf16 and `config.json` declares `torch_dtype:
36
- bfloat16`. The upstream custom `from_pretrained` silently dropped `torch_dtype` and always loaded
37
- fp32; the copy in this repo honors it, so the model loads bf16 natively, like any normal HF
38
- model. Pass `torch_dtype=torch.float32` to load fp32 (note: the stored weights are bf16-precision,
39
- so this only widens the dtype, not the precision).
40
- - **CUDA + `flash_attn`** → flash-varlen path (fast, low VRAM). Attention tensors are cast to bf16
41
- internally as a safety net; with the default bf16 load this is a no-op.
42
- - **CPU, or no `flash_attn`** → the original eager attention algorithm runs unchanged. Because the
43
- model is bf16, eager runs in bf16 here — numerically equivalent to the flash path, just without
44
- its memory and throughput wins. The model loads and encodes on any host; the forward selects the
45
- path automatically (`_FLASH_AVAILABLE and hidden_states.is_cuda`).
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  ## Usage
48
 
@@ -63,35 +87,58 @@ d = model.encode(codes, normalize_embeddings=True)
63
  ## Parity & performance
64
 
65
  The weights are the original CodeRankEmbed weights (bf16-cast), so embeddings match the fp32
66
- original to within bf16 precision. Measured on an RTX 3090 Ti, `flash_attn` 2.8.3, CLS pooling,
67
- L2-normalized, batch size 64:
68
-
69
- | metric | `nomic-ai/CodeRankEmbed` (fp32, eager) | this repo (bf16, flash-varlen) |
70
- | --- | --- | --- |
71
- | cosine vs fp32 reference | 1.000000 | **0.9986** |
72
- | peak VRAM (bs=64) | 6.7 GB | **2.1 GB** (≈3.3× less) |
73
- | throughput (bs=64) | 52,000 tok/s | **162,000 tok/s** (≈3.1× faster) |
74
-
75
- Same 512-snippet corpus (~236k tokens, 20–900 tokens each), batch size 64. The flash-varlen path
76
- also scales to far larger batches (bs=256 fits in ~7.6 GB) where eager O(seq²) blows up. Parity
77
- (>0.997) reproduced on both an RTX 4060 and the 3090 Ti.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  ## What changed vs the source repo
80
 
81
- 1. **Weights**: fp32 → bf16. `flash_attn` only accepts half precision and the model runs bf16 in
82
- any real serving configuration, so the weights are stored bf16 and (via the load fix below)
83
- arrive bf16 — which is simply how this model is used, and removes the need for a post-load dtype
84
- cast. Parity-neutral (cosine 0.9986 vs the fp32 original); the smaller download is incidental,
85
- not the reason.
86
  2. **`from_pretrained` dtype fix**: the upstream custom `from_pretrained` instantiated the model
87
  fp32 and `load_state_dict`-ed the checkpoint into fp32 params, **ignoring `torch_dtype`**. The
88
  copy here adds the standard transformers dtype resolution (explicit arg → `config.torch_dtype` →
89
  checkpoint dtype) so the model loads in its declared dtype.
90
- 3. **Flash-varlen forward**: `NomicBertAttention.forward` gains an `unpad →
91
- flash_attn_varlen_qkvpacked_func(causal=False) → repad` branch (CUDA + flash_attn); the original
92
- eager block is kept as the fallback. `NomicBertModel.forward` skips
93
- `get_extended_attention_mask` on the flash path. Rotary embeddings are applied to the dense
94
- `[B, S, 3, H, D]` tensor **before** unpadding — the correctness keystone.
 
 
 
 
 
 
 
 
95
 
96
  ## License & attribution
97
 
 
16
  # CodeRankEmbed-flash-attn
17
 
18
  A **bf16 quantization of [`nomic-ai/CodeRankEmbed`](https://huggingface.co/nomic-ai/CodeRankEmbed)**
19
+ with a **three-tier attention dispatch built into a custom `modeling_hf_nomic_bert.py` shipped in
20
+ this repo.** It is not a finetune — the weights are the original CodeRankEmbed weights cast to bf16
21
+ (no further training). Two of the three tiers replace the original eager `O(seq²)` attention with an
22
+ `O(N)` unpadded path; the third keeps the original eager algorithm as the correctness reference and
23
+ universal fallback.
24
 
25
  ## Why
26
 
27
  `nomic-ai/CodeRankEmbed` loads through `trust_remote_code`, and its attention path is **eager
28
  only** — activation memory grows as `batch × heads × seq²`, which OOMs at large batches even though
29
+ the model is only 137M params. This repo adds two attention paths that compute the same attention in
30
+ `O(N)` memory by packing unpadded sequences, so the large batches that OOM the eager path run
31
+ comfortably — with **parity embeddings** (no quality change):
32
+
33
+ - `torch_varlen` — `torch.nn.attention.varlen.varlen_attn`, shipped in torch itself (no extra
34
+ package), available from torch **2.10.0** onward.
35
+ - `flash_attn` — the original `flash_attn` varlen-packed kernel. Kept as a fallback for older torch
36
+ builds that don't yet have `torch.nn.attention.varlen` but do have `flash_attn` installed. This
37
+ package is **optional**.
38
+
39
+ Both run the same FA2-family kernel and are gated by the same GPU-capability check. The modeling
40
+ file ships all three paths itself, so no runtime patching or post-load hooks are needed.
41
 
42
  ## Behavior
43
 
44
+ - **Three-tier attention, chosen automatically per device** (override with
45
+ `NOMIC_BERT_ATTN_IMPL=torch_varlen|flash_attn|eager`):
46
+ 1. **`torch_varlen`** — CUDA, compute capability sm_80+ (Ampere or newer), torch **≥ 2.10.0**. No
47
+ third-party kernel needed.
48
+ 2. **`flash_attn`** — CUDA, compute capability sm_80+, the `flash_attn` package importable, on a
49
+ torch that doesn't yet ship `torch.nn.attention.varlen` (typically an older torch). This
50
+ dependency is **optional**.
51
+ 3. **`eager`** — everything else: CPU, pre-Ampere GPUs, or neither of the above available. The
52
+ original padded attention algorithm, unchanged, runs on any host.
53
+
54
+ `auto` (the default) prefers `torch_varlen`, then `flash_attn`, then `eager`. A forced override
55
+ (e.g. `NOMIC_BERT_ATTN_IMPL=torch_varlen`) **raises** `RuntimeError` if that tier's precondition
56
+ doesn't hold — a forced tier never falls back silently. An unrecognized override raises
57
+ `ValueError`.
58
+ - **See which tier engaged**: `model[0].auto_model.attention_impl` after a forward pass, or the
59
+ one-time `NomicBert attention impl=... device=... capability=... torch=... flash_attn=...
60
+ override=...` INFO log line (one line per distinct `(impl, device)`).
61
+ - **Loads bf16 by default.** `flash_attn` and `torch_varlen` both require half precision and the
62
+ model runs bf16 in any real serving setup, so the weights are stored bf16 and `config.json`
63
+ declares `torch_dtype: bfloat16`. The upstream custom `from_pretrained` silently dropped
64
+ `torch_dtype` and always loaded fp32; the copy in this repo honors it, so the model loads bf16
65
+ natively, like any normal HF model. Pass `torch_dtype=torch.float32` to load fp32 (note: the
66
+ stored weights are bf16-precision, so this only widens the dtype, not the precision).
67
+ - **`eager` runs in bf16 too** (because the stored weights are bf16), numerically equivalent to the
68
+ varlen tiers, just without their memory and throughput wins. The model loads and encodes on any
69
+ host regardless of which tier engages.
70
 
71
  ## Usage
72
 
 
87
  ## Parity & performance
88
 
89
  The weights are the original CodeRankEmbed weights (bf16-cast), so embeddings match the fp32
90
+ original to within bf16 precision. The table below re-measures all three tiers after adding the
91
+ dispatch: each tier's output on a 64-code-snippet corpus (fp32-renormalized), compared by cosine
92
+ similarity against this same repo's **pre-dispatch output on the same device** (`flash_attn` on
93
+ CUDA, `eager` on CPU — `flash_attn` can't run on CPU). `eager` and `flash_attn` each run the same
94
+ computation as before the dispatch was added, so their cosines are parity checks (round to
95
+ 1.000000 at 6 decimal places; true values are ≥ 0.9999997). `torch_varlen` is a different,
96
+ torch-native kernel, and its cosine is the real signal.
97
+
98
+ | GPU | tier | device | transformers | mean cos | min cos | peak VRAM |
99
+ | --- | --- | --- | --- | --- | --- | --- |
100
+ | RTX 3090 Ti (sm_86) | `flash_attn` | cuda | 5.16.1 | 1.000000 | 1.000000 | 1145 MiB |
101
+ | RTX 3090 Ti (sm_86) | `torch_varlen` | cuda | 5.16.1 | 0.999941 | 0.999856 | 875 MiB |
102
+ | RTX 3090 Ti (sm_86) | `eager` | cpu | 5.16.1 | 1.000000 | 1.000000 | – (CPU) |
103
+ | RTX 5090 Laptop GPU (sm_120) | `flash_attn` | cuda | 5.11.0 | 1.000000 | 1.000000 | 1169 MiB |
104
+ | RTX 5090 Laptop GPU (sm_120) | `torch_varlen` | cuda | 5.11.0 | 0.999944 | 0.999919 | 899 MiB |
105
+ | RTX 5090 Laptop GPU (sm_120) | `eager` | cpu | 5.11.0 | 1.000000 | 1.000000 | – (CPU) |
106
+ | CPU only | `eager` | cpu | 5.17.0 | 1.000000 | 1.000000 | – (CPU) |
107
+
108
+ Cosines rounded to 6 decimal places; peak VRAM rounded to the nearest MiB. `eager` always runs on
109
+ CPU in this protocol (it is the universal fallback tier); the GPU named in the first column is the
110
+ host each row's measurement ran on, not the device `eager` used on that row. `torch` was
111
+ `2.12.1+cu130` for every row except the standalone CPU-only row (`torch 2.14.0+cpu`), which used a
112
+ separate `transformers==5.17.0` install to check the eager path against a newer transformers.
113
+
114
+ Separately, both varlen tiers (`torch_varlen`, `flash_attn`) are gated in this repo's downstream
115
+ test suite at min cosine > 0.997 against the fp32 `nomic-ai/CodeRankEmbed` reference (not
116
+ re-measured here — see the table above for this repo's own numbers), and stay under 20 GB peak VRAM
117
+ at batch size 256.
118
 
119
  ## What changed vs the source repo
120
 
121
+ 1. **Weights**: fp32 → bf16. `flash_attn` and `torch_varlen` only accept half precision and the
122
+ model runs bf16 in any real serving configuration, so the weights are stored bf16 and (via the
123
+ load fix below) arrive bf16 — which is simply how this model is used, and removes the need for a
124
+ post-load dtype cast. Parity-neutral; the smaller download is incidental, not the reason.
 
125
  2. **`from_pretrained` dtype fix**: the upstream custom `from_pretrained` instantiated the model
126
  fp32 and `load_state_dict`-ed the checkpoint into fp32 params, **ignoring `torch_dtype`**. The
127
  copy here adds the standard transformers dtype resolution (explicit arg → `config.torch_dtype` →
128
  checkpoint dtype) so the model loads in its declared dtype.
129
+ 3. **Three-tier attention dispatch**: `NomicBertAttention.forward` now selects one of three
130
+ attention implementations at call time — `torch_varlen` (torch's own
131
+ `torch.nn.attention.varlen.varlen_attn`, no third-party kernel, needs torch ≥ 2.10.0),
132
+ `flash_attn` (the original flash-attn varlen-packed kernel, kept as a fallback for older torch
133
+ builds that have it installed), and `eager` (the original padded attention, unchanged, and the
134
+ default off CUDA sm_80+). Both varlen tiers unpad the input with torch-native `_unpad`/`_pad`
135
+ helpers (a replacement for `flash_attn.bert_padding`) before the packed kernel call, then repad
136
+ the output; `NomicBertModel.forward` builds the additive attention mask inline instead of calling
137
+ the (now-removed-upstream) `get_extended_attention_mask` helper. Rotary embeddings are applied to
138
+ the dense `[B, S, 3, H, D]` tensor **before** unpadding — the correctness keystone: applying RoPE
139
+ after unpadding would hand each packed position the wrong sequence's rotation. Set
140
+ `NOMIC_BERT_ATTN_IMPL=torch_varlen|flash_attn|eager` to force a tier (raises if it can't engage);
141
+ `model[0].auto_model.attention_impl` and the one-time INFO log line report which tier engaged.
142
 
143
  ## License & attribution
144
 
modeling_hf_nomic_bert.py CHANGED
@@ -6,6 +6,8 @@
6
  import logging
7
 
8
  # Inspired by https://github.com/huggingface/transformers/blob/main/src/transformers/models/bert/modeling_bert.py
 
 
9
  import os
10
  import re
11
  from collections import OrderedDict
@@ -26,27 +28,179 @@ from transformers.models.bert.modeling_bert import (
26
  from transformers.utils import SAFE_WEIGHTS_INDEX_NAME, SAFE_WEIGHTS_NAME, WEIGHTS_INDEX_NAME, WEIGHTS_NAME
27
  from transformers.utils.hub import cached_file, get_checkpoint_shard_files
28
 
29
- # Flash-attention varlen path (optional). When ``flash_attn`` is importable and
30
- # the tensors live on CUDA, attention runs as unpadded varlen packing — O(N)
31
- # activation memory instead of the eager path's O(seq^2). When flash_attn is
32
- # unavailable (CPU-only hosts, or builds without it) the original eager path
33
- # runs byte-for-byte unchanged, so this model loads and encodes everywhere.
 
 
 
 
 
 
 
 
 
 
 
 
34
  try: # pragma: no cover - import guard, exercised by environment
35
  from flash_attn import flash_attn_varlen_qkvpacked_func
36
- from flash_attn.bert_padding import pad_input, unpad_input
37
 
38
  _FLASH_AVAILABLE = True
39
  except Exception: # pragma: no cover
40
  flash_attn_varlen_qkvpacked_func = None # type: ignore[assignment]
41
- pad_input = None # type: ignore[assignment]
42
- unpad_input = None # type: ignore[assignment]
43
  _FLASH_AVAILABLE = False
44
 
45
- from .configuration_hf_nomic_bert import NomicBertConfig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
 
47
  logger = logging.getLogger(__name__)
48
 
49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  # adapted from flash attention, added safe serialization option for hf models
51
  def state_dict_from_pretrained(model_name, safe_serialization=False, device=None, dtype=None):
52
  # If not fp32, then we don't want to load directly to the GPU
@@ -813,13 +967,14 @@ class NomicBertAttention(nn.Module):
813
  if self.rotary_head_dim:
814
  qkv = rearrange(qkv, "b h three s d -> b s three h d")
815
 
816
- if _FLASH_AVAILABLE and hidden_states.is_cuda:
817
- # --- Flash-attention varlen path ---
818
- # flash_attn derives per-sequence lengths from ``cu_seqlens`` (built by
819
- # unpad_input), so it consumes the raw bool [B, S] mask — NOT the
820
- # additive [B, 1, 1, S] mask that get_extended_attention_mask makes.
821
- # NomicBertModel.forward skips get_extended_attention_mask when flash is
822
- # active and passes the bool mask straight through.
 
823
  #
824
  # Correctness keystone: RoPE was already applied above, on the dense
825
  # [B, S, 3, H, D] tensor, BEFORE unpadding — so per-sequence positions
@@ -827,17 +982,15 @@ class NomicBertAttention(nn.Module):
827
  # absolute positions of sequence 1's tail and silently drift embeddings.
828
  B, S = qkv.shape[0], qkv.shape[1]
829
 
830
- # flash_attn requires fp16/bf16 inputs. sentence-transformers loads the
831
- # weights as fp32 by default (it silently drops config.torch_dtype), so
832
- # if the model is fp32 we cast qkv to bf16 for the flash call and cast
833
- # the result back to the model dtype before out_proj. This makes the
834
- # model work regardless of how it was loaded — no external cast needed.
 
835
  orig_dtype = qkv.dtype
836
- flash_dtype = (
837
- orig_dtype if orig_dtype in (torch.float16, torch.bfloat16) else torch.bfloat16
838
- )
839
- if flash_dtype != orig_dtype:
840
- qkv = qkv.to(flash_dtype)
841
 
842
  if attention_mask is None:
843
  bool_mask = torch.ones(B, S, device=qkv.device, dtype=torch.bool)
@@ -847,7 +1000,7 @@ class NomicBertAttention(nn.Module):
847
  else:
848
  bool_mask = attention_mask.to(torch.bool)
849
 
850
- qkv_u, indices, cu_s, max_s, *_ = unpad_input(qkv, bool_mask)
851
  if max_s == 0:
852
  raise RuntimeError(
853
  "NomicBertAttention.forward: every sequence is empty (max_s == 0); "
@@ -855,15 +1008,19 @@ class NomicBertAttention(nn.Module):
855
  )
856
  # causal=False: bidirectional encoder. Default softmax_scale is
857
  # 1/sqrt(head_dim), numerically identical to / self.norm_factor above.
858
- out_u = flash_attn_varlen_qkvpacked_func(
859
- qkv_u, cu_s, max_s, dropout_p=0.0, causal=False
860
- )
861
- attn_output = pad_input(out_u, indices, B, S) # [B, S, H, D]
 
 
 
 
862
  attn_output = attn_output.to(orig_dtype) # restore model dtype for out_proj
863
  attn_output = rearrange(attn_output, "... h d -> ... (h d)")
864
  return self.out_proj(attn_output)
865
 
866
- # --- Eager path (fallback: CPU, or flash_attn unavailable) ---
867
  # Byte-for-byte identical to nomic-ai/CodeRankEmbed upstream.
868
  query, key, value = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2]
869
 
@@ -1110,6 +1267,7 @@ class NomicBertPreTrainingHeads(nn.Module):
1110
  class NomicBertModel(NomicBertPreTrainedModel):
1111
  def __init__(self, config: GPT2Config, add_pooling_layer=True):
1112
  super().__init__(config)
 
1113
  self.pad_vocab_size_multiple = getattr(config, "pad_vocab_size_multiple", 1)
1114
  if config.vocab_size % self.pad_vocab_size_multiple != 0:
1115
  config.vocab_size += self.pad_vocab_size_multiple - (config.vocab_size % self.pad_vocab_size_multiple)
@@ -1146,20 +1304,18 @@ class NomicBertModel(NomicBertPreTrainedModel):
1146
  hidden_states = self.emb_ln(hidden_states)
1147
  hidden_states = self.emb_drop(hidden_states)
1148
 
1149
- if _FLASH_AVAILABLE and hidden_states.is_cuda:
1150
- # Flash-varlen path: pass the raw bool [B, S] mask straight through.
1151
- # Do NOT call get_extended_attention_mask here — it produces an
1152
- # additive [B, 1, 1, S] mask that flash_attn (which derives lengths
1153
- # from cu_seqlens via unpad_input) cannot consume.
1154
- sequence_output = self.encoder(
1155
- hidden_states, attention_mask=attention_mask, return_dict=return_dict
1156
- )
1157
- else:
1158
- # Eager path: original additive [B, 1, 1, S] extended mask.
1159
- attention_mask = self.get_extended_attention_mask(attention_mask, input_ids.shape)
1160
- sequence_output = self.encoder(
1161
- hidden_states, attention_mask=attention_mask, return_dict=return_dict
1162
- )
1163
 
1164
  pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
1165
 
 
6
  import logging
7
 
8
  # Inspired by https://github.com/huggingface/transformers/blob/main/src/transformers/models/bert/modeling_bert.py
9
+ import functools
10
+ import importlib.metadata
11
  import os
12
  import re
13
  from collections import OrderedDict
 
28
  from transformers.utils import SAFE_WEIGHTS_INDEX_NAME, SAFE_WEIGHTS_NAME, WEIGHTS_INDEX_NAME, WEIGHTS_NAME
29
  from transformers.utils.hub import cached_file, get_checkpoint_shard_files
30
 
31
+ from .configuration_hf_nomic_bert import NomicBertConfig
32
+
33
+ # Three-tier attention dispatch (auto by default, override via NOMIC_BERT_ATTN_IMPL):
34
+ # 1. torch_varlen — torch.nn.attention.varlen, no third-party kernel
35
+ # 2. flash_attn — the flash-attn varlen-packed kernel (optional dependency)
36
+ # 3. eager — the original padded path, byte-for-byte, runs everywhere
37
+ # Both tier 1 and tier 2 run the same FA2-family kernel and refuse pre-Ampere
38
+ # hardware, so a single capability predicate gates both (resolve_attention_impl,
39
+ # below). When neither is available (CPU-only hosts, or builds without them)
40
+ # tier 3 runs unchanged, so this model loads and encodes everywhere.
41
+ try: # pragma: no cover - import guard, exercised by environment
42
+ from torch.nn.attention.varlen import varlen_attn as _torch_varlen_attn
43
+
44
+ _TORCH_VARLEN_AVAILABLE = True
45
+ except ImportError: # pragma: no cover
46
+ _torch_varlen_attn = None # type: ignore[assignment]
47
+ _TORCH_VARLEN_AVAILABLE = False
48
  try: # pragma: no cover - import guard, exercised by environment
49
  from flash_attn import flash_attn_varlen_qkvpacked_func
 
50
 
51
  _FLASH_AVAILABLE = True
52
  except Exception: # pragma: no cover
53
  flash_attn_varlen_qkvpacked_func = None # type: ignore[assignment]
 
 
54
  _FLASH_AVAILABLE = False
55
 
56
+ ATTENTION_IMPLS = ("torch_varlen", "flash_attn", "eager")
57
+ ATTN_IMPL_ENV = "NOMIC_BERT_ATTN_IMPL" # auto (default) | torch_varlen | flash_attn | eager
58
+
59
+
60
+ def resolve_attention_impl(
61
+ *,
62
+ override: str,
63
+ torch_varlen_available: bool,
64
+ flash_available: bool,
65
+ is_cuda: bool,
66
+ capability: Optional[Tuple[int, int]],
67
+ ) -> str:
68
+ """Pick an attention tier from an override, availability flags, and device info.
69
+
70
+ ``varlen_ok`` gates both ``torch_varlen`` and ``flash_attn``: both run the FA2
71
+ kernel family, which refuses pre-Ampere hardware. ``override == "auto"`` prefers
72
+ ``torch_varlen``, then ``flash_attn``, then ``eager``. A named override returns
73
+ that tier if its precondition holds, else raises ``RuntimeError`` naming the
74
+ tier and the failed precondition — a forced tier never falls back silently.
75
+ Any other override raises ``ValueError`` listing ``ATTENTION_IMPLS``.
76
+ """
77
+ if override != "auto" and override not in ATTENTION_IMPLS:
78
+ raise ValueError(f"{ATTN_IMPL_ENV}={override!r}: expected 'auto' or one of {ATTENTION_IMPLS}")
79
+ varlen_ok = is_cuda and capability is not None and tuple(capability) >= (8, 0)
80
+ if override == "auto":
81
+ if torch_varlen_available and varlen_ok:
82
+ return "torch_varlen"
83
+ return "flash_attn" if flash_available and varlen_ok else "eager"
84
+ if override == "eager":
85
+ return "eager"
86
+ if override == "torch_varlen" and not torch_varlen_available:
87
+ raise RuntimeError(
88
+ f"{ATTN_IMPL_ENV}=torch_varlen requested but torch.nn.attention.varlen "
89
+ f"is unavailable (torch {torch.__version__}; needs >= 2.10)"
90
+ )
91
+ if override == "flash_attn" and not flash_available:
92
+ raise RuntimeError(f"{ATTN_IMPL_ENV}=flash_attn requested but flash_attn is not importable")
93
+ if not is_cuda:
94
+ raise RuntimeError(f"{ATTN_IMPL_ENV}={override} requested but device is cpu (not CUDA)")
95
+ if capability is None or tuple(capability) < (8, 0):
96
+ raise RuntimeError(
97
+ f"{ATTN_IMPL_ENV}={override} requested but compute capability "
98
+ f"{tuple(capability) if capability else None} < (8, 0)"
99
+ )
100
+ return override
101
+
102
+
103
+ @functools.lru_cache(maxsize=None)
104
+ def _select_cached(dev_type, dev_index, override):
105
+ is_cuda = dev_type == "cuda"
106
+ cap = torch.cuda.get_device_capability(torch.device(dev_type, dev_index)) if is_cuda else None
107
+ return resolve_attention_impl(
108
+ override=override,
109
+ torch_varlen_available=_TORCH_VARLEN_AVAILABLE,
110
+ flash_available=_FLASH_AVAILABLE,
111
+ is_cuda=is_cuda,
112
+ capability=cap,
113
+ )
114
+
115
+
116
+ def _unpad(x: torch.Tensor, mask: torch.Tensor):
117
+ """Flatten ``[B,S,...]`` to the packed ``[N,...]`` of true positions in ``mask``.
118
+
119
+ Torch-native replacement for flash-attn's ``bert_padding.unpad_input``. Returns
120
+ ``(x_u, indices, cu_seqlens, max_seqlen)``: ``indices`` (int64) locate each
121
+ packed row in the flattened ``B*S``; ``cu_seqlens`` (int32, length ``B+1``) is
122
+ the exclusive prefix sum of per-row valid lengths, as the varlen kernels need.
123
+ """
124
+ seqlens = mask.sum(-1, dtype=torch.int32)
125
+ indices = torch.nonzero(mask.flatten(), as_tuple=False).flatten()
126
+ max_s = int(seqlens.max())
127
+ cu = F.pad(torch.cumsum(seqlens, 0, dtype=torch.int32), (1, 0))
128
+ return x.flatten(0, 1)[indices], indices, cu, max_s
129
+
130
+
131
+ def _pad(x_u: torch.Tensor, indices: torch.Tensor, B: int, S: int) -> torch.Tensor:
132
+ """Scatter the packed ``[N,...]`` back to ``[B,S,...]`` (inverse of ``_unpad``).
133
+
134
+ Torch-native replacement for flash-attn's ``bert_padding.pad_input``. Padding
135
+ positions are zero-filled.
136
+ """
137
+ out = torch.zeros(B * S, *x_u.shape[1:], device=x_u.device, dtype=x_u.dtype)
138
+ out[indices] = x_u
139
+ return out.view(B, S, *x_u.shape[1:])
140
+
141
+
142
+ def select_attention_impl(device: torch.device) -> str:
143
+ """Bind ``resolve_attention_impl`` to a device, the env override and a memo.
144
+
145
+ Reads ``NOMIC_BERT_ATTN_IMPL`` on every call (so a forced-unavailable tier
146
+ keeps raising rather than sticking to a memoized exception — ``lru_cache``
147
+ never caches a raised call) and memoizes the resolved tier per
148
+ ``(device.type, device.index, override)``.
149
+ """
150
+ override = os.environ.get(ATTN_IMPL_ENV) or "auto"
151
+ return _select_cached(device.type, device.index, override)
152
+
153
 
154
  logger = logging.getLogger(__name__)
155
 
156
 
157
+ def _inline_extended_mask(attention_mask: Optional[torch.Tensor], dtype: torch.dtype) -> Optional[torch.Tensor]:
158
+ """Build the additive ``[B, 1, 1, S]`` mask the eager tier consumes.
159
+
160
+ Replaces the ``PreTrainedModel`` helper, which newer transformers removed,
161
+ with the same arithmetic inline, so this file has no dependency on that
162
+ helper. ``None`` stays ``None``.
163
+ """
164
+ if attention_mask is None:
165
+ return None
166
+ return (1.0 - attention_mask[:, None, None, :].to(dtype)) * torch.finfo(dtype).min
167
+
168
+
169
+ _LOGGED_IMPLS: set = set()
170
+
171
+
172
+ def _log_attention_impl_once(impl: str, device: torch.device) -> None:
173
+ """Emit one INFO line per ``(impl, device)`` naming why this tier engaged.
174
+
175
+ INFO, not WARNING, on every tier: eager on CPU is documented behaviour, not a
176
+ degradation. The line names the reason (capability, torch version,
177
+ flash_attn presence, the active override) so an operator reading a worker
178
+ log can tell why a faster tier was or wasn't selected.
179
+ """
180
+ key = (impl, str(device))
181
+ if key in _LOGGED_IMPLS:
182
+ return
183
+ _LOGGED_IMPLS.add(key)
184
+ is_cuda = device.type == "cuda"
185
+ cap = torch.cuda.get_device_capability(device) if is_cuda else None
186
+ if _FLASH_AVAILABLE:
187
+ try:
188
+ fa = importlib.metadata.version("flash-attn")
189
+ except importlib.metadata.PackageNotFoundError:
190
+ fa = "unknown"
191
+ else:
192
+ fa = "absent"
193
+ logger.info(
194
+ "NomicBert attention impl=%s device=%s capability=%s torch=%s flash_attn=%s override=%s",
195
+ impl,
196
+ device,
197
+ cap,
198
+ torch.__version__,
199
+ fa,
200
+ os.environ.get(ATTN_IMPL_ENV) or "auto",
201
+ )
202
+
203
+
204
  # adapted from flash attention, added safe serialization option for hf models
205
  def state_dict_from_pretrained(model_name, safe_serialization=False, device=None, dtype=None):
206
  # If not fp32, then we don't want to load directly to the GPU
 
967
  if self.rotary_head_dim:
968
  qkv = rearrange(qkv, "b h three s d -> b s three h d")
969
 
970
+ impl = select_attention_impl(hidden_states.device)
971
+ if impl != "eager":
972
+ # --- torch_varlen / flash_attn varlen paths ---
973
+ # Both consume the raw bool [B, S] mask (per-sequence lengths derived
974
+ # from cu_seqlens) — NOT the additive [B, 1, 1, S] mask that
975
+ # NomicBertModel.forward builds inline (_inline_extended_mask) for the
976
+ # eager tier. NomicBertModel.forward calls the same select_attention_impl
977
+ # and, on a varlen tier, passes this raw bool mask straight through.
978
  #
979
  # Correctness keystone: RoPE was already applied above, on the dense
980
  # [B, S, 3, H, D] tensor, BEFORE unpadding — so per-sequence positions
 
982
  # absolute positions of sequence 1's tail and silently drift embeddings.
983
  B, S = qkv.shape[0], qkv.shape[1]
984
 
985
+ # Both varlen kernels require fp16/bf16 inputs. sentence-transformers
986
+ # loads the weights as fp32 by default (it silently drops
987
+ # config.torch_dtype), so if the model is fp32 we cast qkv to bf16 for
988
+ # the kernel call and cast the result back to the model dtype before
989
+ # out_proj. This makes the model work regardless of how it was loaded —
990
+ # no external cast needed.
991
  orig_dtype = qkv.dtype
992
+ if orig_dtype not in (torch.float16, torch.bfloat16):
993
+ qkv = qkv.to(torch.bfloat16)
 
 
 
994
 
995
  if attention_mask is None:
996
  bool_mask = torch.ones(B, S, device=qkv.device, dtype=torch.bool)
 
1000
  else:
1001
  bool_mask = attention_mask.to(torch.bool)
1002
 
1003
+ qkv_u, indices, cu_s, max_s = _unpad(qkv, bool_mask)
1004
  if max_s == 0:
1005
  raise RuntimeError(
1006
  "NomicBertAttention.forward: every sequence is empty (max_s == 0); "
 
1008
  )
1009
  # causal=False: bidirectional encoder. Default softmax_scale is
1010
  # 1/sqrt(head_dim), numerically identical to / self.norm_factor above.
1011
+ if impl == "torch_varlen":
1012
+ q, k, v = qkv_u.unbind(1)
1013
+ out_u = _torch_varlen_attn(q, k, v, cu_s, cu_s, max_s, max_s)
1014
+ else:
1015
+ out_u = flash_attn_varlen_qkvpacked_func(
1016
+ qkv_u, cu_s, max_s, dropout_p=0.0, causal=False
1017
+ )
1018
+ attn_output = _pad(out_u, indices, B, S) # [B, S, H, D]
1019
  attn_output = attn_output.to(orig_dtype) # restore model dtype for out_proj
1020
  attn_output = rearrange(attn_output, "... h d -> ... (h d)")
1021
  return self.out_proj(attn_output)
1022
 
1023
+ # --- Eager path (no varlen tier engaged: CPU, pre-Ampere, forced, or no kernel) ---
1024
  # Byte-for-byte identical to nomic-ai/CodeRankEmbed upstream.
1025
  query, key, value = qkv[:, :, 0], qkv[:, :, 1], qkv[:, :, 2]
1026
 
 
1267
  class NomicBertModel(NomicBertPreTrainedModel):
1268
  def __init__(self, config: GPT2Config, add_pooling_layer=True):
1269
  super().__init__(config)
1270
+ self.attention_impl = None
1271
  self.pad_vocab_size_multiple = getattr(config, "pad_vocab_size_multiple", 1)
1272
  if config.vocab_size % self.pad_vocab_size_multiple != 0:
1273
  config.vocab_size += self.pad_vocab_size_multiple - (config.vocab_size % self.pad_vocab_size_multiple)
 
1304
  hidden_states = self.emb_ln(hidden_states)
1305
  hidden_states = self.emb_drop(hidden_states)
1306
 
1307
+ impl = select_attention_impl(hidden_states.device)
1308
+ self.attention_impl = impl
1309
+ _log_attention_impl_once(impl, hidden_states.device)
1310
+ if impl == "eager":
1311
+ # No varlen tier engaged (CPU, pre-Ampere, forced, or no kernel):
1312
+ # build the additive [B, 1, 1, S] mask inline for the eager block.
1313
+ attention_mask = _inline_extended_mask(attention_mask, hidden_states.dtype)
1314
+ # else: torch_varlen / flash_attn consume the raw bool [B, S] mask as-is —
1315
+ # NomicBertAttention.forward derives lengths from it via cu_seqlens.
1316
+ sequence_output = self.encoder(
1317
+ hidden_states, attention_mask=attention_mask, return_dict=return_dict
1318
+ )
 
 
1319
 
1320
  pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
1321