handwoven8588 commited on
Commit
ee5869f
·
verified ·
1 Parent(s): 5265f03

Padding-free encoder, flattening opt-in, torch.compile recipe; card measured on public corpora

Browse files
Files changed (2) hide show
  1. README.md +194 -62
  2. modeling_hf_nomic_bert.py +178 -5
README.md CHANGED
@@ -65,6 +65,17 @@ file ships all three paths itself, so no runtime patching or post-load hooks are
65
  override=...` INFO log line (one line per distinct `(impl, device)`). When `flash_attn` is
66
  unavailable the line names why (`flash_attn=absent(<error>)`); an installed `flash_attn` that
67
  fails to import also logs a WARNING.
 
 
 
 
 
 
 
 
 
 
 
68
  - **`revision=` pins everything.** Loading with `revision=<commit>` fetches the code, config,
69
  tokenizer **and weights** from that commit.
70
  - **Loads bf16 by default.** `flash_attn` and `torch_varlen` both require half precision and the
@@ -93,73 +104,188 @@ q = model.encode(queries, normalize_embeddings=True)
93
  d = model.encode(codes, normalize_embeddings=True)
94
  ```
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  ## Parity & performance
97
 
98
  The weights are the original CodeRankEmbed weights (bf16-cast), so embeddings match the fp32
99
  original to within bf16 precision. The table below measures every tier **on the same GPU**, so
100
  `eager` is the O(seq²) baseline the varlen tiers are compared against.
101
 
102
- **Protocol.** 64 real Python source snippets (the first 40 lines of the first 64 `.py` files,
103
- by path, of the PenStock inference library's source tree; 428 tokens mean, 638 max), encoded
104
- with sentence-transformers at an explicit **batch size of 32 and of 64** (the
105
- `batch_size=` passed to `encode`; with 64 snippets, 64 is one forward). Each tier encodes once
106
- untimed, then again for the measurement. Cosine similarity is taken on fp32-renormalized output
107
- against the **pre-dispatch revision of this repo on the same GPU** (its `flash_attn` path).
108
- `flash_attn` runs the same computation as before the dispatch, so its cosine is a parity check.
109
- `torch_varlen` is a different, torch-native kernel, and `eager` a different algorithm, so theirs
110
- are the real signal. `auto` is the default with no override: the probe keeps `torch_varlen`.
111
- **Encode peak** is the encode's peak CUDA allocation above the loaded model's own ~266 MiB of
112
- bf16 weights. Wall time is one encode of all 64 snippets.
113
-
114
- | GPU | batch size | tier | mean cos | min cos | encode peak | wall |
115
- | --- | --- | --- | --- | --- | --- | --- |
116
- | RTX 3090 Ti (sm_86) | 32 | `torch_varlen` | 0.999941 | 0.999856 | 600 MiB | 0.197 s |
117
- | RTX 3090 Ti (sm_86) | 32 | `flash_attn` | 1.000000 | 1.000000 | 600 MiB | 0.197 s |
118
- | RTX 3090 Ti (sm_86) | 32 | `eager` | 0.999912 | 0.999856 | 807 MiB | 0.262 s |
119
- | RTX 3090 Ti (sm_86) | 64 | `torch_varlen` | 0.999941 | 0.999856 | 1201 MiB | 0.223 s |
120
- | RTX 3090 Ti (sm_86) | 64 | `flash_attn` | 1.000000 | 1.000000 | 1201 MiB | 0.218 s |
121
- | RTX 3090 Ti (sm_86) | 64 | `eager` | 0.999910 | 0.999856 | 1614 MiB | 0.302 s |
122
- | RTX 5090 Laptop GPU (sm_120) | 32 | `torch_varlen` | 0.999944 | 0.999919 | 600 MiB | 0.177 s |
123
- | RTX 5090 Laptop GPU (sm_120) | 32 | `flash_attn` | 1.000000 | 1.000000 | 600 MiB | 0.175 s |
124
- | RTX 5090 Laptop GPU (sm_120) | 32 | `eager` | 0.999912 | 0.999870 | 807 MiB | 0.249 s |
125
- | RTX 5090 Laptop GPU (sm_120) | 64 | `torch_varlen` | 0.999944 | 0.999919 | 1201 MiB | 0.202 s |
126
- | RTX 5090 Laptop GPU (sm_120) | 64 | `flash_attn` | 1.000000 | 1.000000 | 1201 MiB | 0.203 s |
127
- | RTX 5090 Laptop GPU (sm_120) | 64 | `eager` | 0.999913 | 0.999834 | 1614 MiB | 0.298 s |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  Cosines rounded to 6 decimal places, VRAM to the nearest MiB. `torch 2.12.1+cu130`,
130
  `transformers 5.11.0`, `sentence-transformers 6.1.0`, `flash-attn 2.8.3` (`2.8.3.post1` on the
131
- 3090 Ti), all measured loading this repo at `1954de4`.
132
 
133
- Eager's cost grows with `batch × heads × seq²`: at these short snippets it needs about a third more
134
- encode memory than the varlen tiers. At longer inputs the gap stops being a percentage.
 
 
 
 
 
 
 
 
 
135
 
136
  **Real documents at the model defaults.** 768 Python files sampled with a fixed seed from one shard
137
  of [`HuggingFaceTB/stack-edu`](https://huggingface.co/datasets/HuggingFaceTB/stack-edu) (730,431
138
  tokens after truncation), encoded with sentence-transformers at this model's default
139
- `max_seq_length` of 8192 and one `encode()` call per row, so the files are length-sorted before
140
- batching. Wall time covers the whole corpus; peak VRAM is the peak CUDA allocation, weights
141
- included. The largest batch row is the largest batch size that fit on each card (the 3090 Ti was
142
- sharing about 1 GiB with other processes).
143
-
144
- | GPU | batch size | tier | wall (768 files) | peak VRAM |
145
- | --- | --- | --- | --- | --- |
146
- | RTX 3090 Ti (sm_86) | 4 | `torch_varlen` | 6.57 s | 1,295 MiB |
147
- | RTX 3090 Ti (sm_86) | 4 | `flash_attn` | 6.58 s | 1,296 MiB |
148
- | RTX 3090 Ti (sm_86) | 4 | `eager` | 14.18 s | 12,961 MiB |
149
- | RTX 3090 Ti (sm_86) | 32 | `torch_varlen` | 6.37 s | 7,973 MiB |
150
- | RTX 3090 Ti (sm_86) | 64 | `torch_varlen` | 7.24 s | 15,659 MiB |
151
- | RTX 3090 Ti (sm_86) | 71 | `torch_varlen` | 7.60 s | 17,340 MiB |
152
- | RTX 5090 Laptop GPU (sm_120) | 4 | `torch_varlen` | 5.54 s | 1,319 MiB |
153
- | RTX 5090 Laptop GPU (sm_120) | 4 | `flash_attn` | 5.57 s | 1,319 MiB |
154
- | RTX 5090 Laptop GPU (sm_120) | 4 | `eager` | 16.27 s | 12,983 MiB |
155
- | RTX 5090 Laptop GPU (sm_120) | 32 | `torch_varlen` | 6.36 s | 7,995 MiB |
156
- | RTX 5090 Laptop GPU (sm_120) | 64 | `torch_varlen` | 7.48 s | 15,681 MiB |
157
- | RTX 5090 Laptop GPU (sm_120) | 73 | `torch_varlen` | 7.83 s | 17,843 MiB |
158
-
159
- At batch 4 `eager` takes 2.2× (3090 Ti) to 2.9× (5090 Laptop) as long as the varlen tiers and needs
160
- about ten times the memory. At batch 32 with the same files in random order, `eager` ran out of
161
- memory on 21 of the 24 batches (3090 Ti). Both varlen tiers are also gated downstream at min
162
- cosine > 0.997 against the fp32 `nomic-ai/CodeRankEmbed` reference.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
 
164
  ## What changed vs the source repo
165
 
@@ -178,19 +304,25 @@ cosine > 0.997 against the fp32 `nomic-ai/CodeRankEmbed` reference.
178
  builds that have it installed), and `eager` (the original padded attention algorithm, numerically
179
  unchanged, and the default off CUDA sm_80+; it now converts a raw 2-D `[B, S]` mask to the
180
  additive form before adding it, for the case where a `device_map` split hands it a mask a varlen
181
- tier upstream never passes). Both varlen tiers unpad the input with torch-native `_unpad`/`_pad`
182
- helpers (a replacement for `flash_attn.bert_padding`) before the packed kernel call, then repad
183
- the output; `NomicBertModel.forward` builds the additive attention mask inline instead of calling
184
- the (now-removed-upstream) `get_extended_attention_mask` helper. Rotary embeddings are applied to
185
- the dense `[B, S, 3, H, D]` tensor **before** unpadding — the correctness keystone: applying RoPE
186
- after unpadding would hand each packed position the wrong sequence's rotation. Set
187
- `NOMIC_BERT_ATTN_IMPL=torch_varlen|flash_attn|eager` to force a tier (raises if it can't engage);
188
- `model[0].auto_model.attention_impl` and the one-time INFO log line report which tier engaged.
 
189
  4. **`from_pretrained` forwards the Hub revision**: the upstream custom `from_pretrained` fetched
190
  the weight file without the caller's `revision` (and cache/token options), so a pinned load
191
  still took the weights from the default branch. The copy here passes them through, so a
192
  pinned load fetches the weights from the pinned commit too. It also accepts transformers'
193
- `dtype=` alongside the older `torch_dtype=`.
 
 
 
 
 
194
 
195
  ## License & attribution
196
 
 
65
  override=...` INFO log line (one line per distinct `(impl, device)`). When `flash_attn` is
66
  unavailable the line names why (`flash_attn=absent(<error>)`); an installed `flash_attn` that
67
  fails to import also logs a WARNING.
68
+ - **Padding-free on the varlen tiers.** The model unpads the batch once, right after the input
69
+ ids, runs the embeddings and every encoder layer on the real tokens only, and pads back once at
70
+ the end. `last_hidden_state` keeps its `[B, S, 768]` shape, with zeros at padding positions, so
71
+ pooling is unchanged.
72
+ - **Flattened batches (opt-in).** Loading with
73
+ `model_kwargs={"attn_implementation": "flash_attention_2"}` lets sentence-transformers send each
74
+ batch flattened (`[1, ΣL]` token ids with sequence boundaries, no padding at all), and the model
75
+ returns one `[1, ΣL, 768]` output that sentence-transformers pools per sequence. The opt-in needs
76
+ the `flash_attn` package installed, because transformers checks for it at load and raises
77
+ otherwise, even though the attention itself still runs on whichever tier the dispatch picks. On
78
+ the `eager` tier the flattened batch is re-padded and runs the padded path.
79
  - **`revision=` pins everything.** Loading with `revision=<commit>` fetches the code, config,
80
  tokenizer **and weights** from that commit.
81
  - **Loads bf16 by default.** `flash_attn` and `torch_varlen` both require half precision and the
 
104
  d = model.encode(codes, normalize_embeddings=True)
105
  ```
106
 
107
+ With `flash_attn` installed, opt in to flattened batches:
108
+
109
+ ```python
110
+ model = SentenceTransformer(
111
+ "handwoven8588/CodeRankEmbed-flash-attn",
112
+ trust_remote_code=True,
113
+ model_kwargs={"attn_implementation": "flash_attention_2"},
114
+ )
115
+ ```
116
+
117
+ On a varlen tier the encoder layers run on packed tokens only, so they compile cleanly. For about
118
+ 1.2× the throughput and a third less peak memory (see [torch.compile](#torchcompile) below):
119
+
120
+ ```python
121
+ import torch
122
+
123
+ model[0].auto_model.encoder = torch.compile(model[0].auto_model.encoder, dynamic=True)
124
+ ```
125
+
126
+ `dynamic=True` matters: the number of packed tokens changes with every batch.
127
+
128
  ## Parity & performance
129
 
130
  The weights are the original CodeRankEmbed weights (bf16-cast), so embeddings match the fp32
131
  original to within bf16 precision. The table below measures every tier **on the same GPU**, so
132
  `eager` is the O(seq²) baseline the varlen tiers are compared against.
133
 
134
+ **Protocol.** The first 16,384 `document` fields (real Python functions) of
135
+ [`lightonai/cornstack`](https://huggingface.co/datasets/lightonai/cornstack)'s Python split, in
136
+ shard order (`train-00000-of-00682.parquet`, then `train-00001-…`): 2,684,878 tokens, 7 to 4,746
137
+ per function, 164 mean, 92 median. They are encoded with sentence-transformers in one `encode()`
138
+ call per row at the stated `batch_size=` (64 to 4,096 batches per row), under the default load
139
+ and under the flattened opt-in. Each row encodes once untimed, then again for the measurement.
140
+ Cosine similarity is taken on fp32-renormalized output against one encode by the **pre-dispatch
141
+ revision of this repo (`e361c6f`) on the same GPU at batch 32**, which runs the original
142
+ `flash_attn` path on padded batches. `torch_varlen` is a different, torch-native kernel and
143
+ `eager` a different algorithm, so their cosines measure those differences. `flash_attn` is the
144
+ same attention kernel; where it sits below 1.000000, the rest of the network runs on packed
145
+ tokens or on differently composed batches, so its matrix shapes, and with them its bf16
146
+ rounding, differ. `auto`, the default with no override, kept `torch_varlen` on both cards and
147
+ matches its rows. **Encode peak** is the encode's peak CUDA allocation above what was allocated
148
+ before it (the loaded model's ~266 MiB of bf16 weights). Wall time covers all 16,384 functions.
149
+ `eager` runs at batch 4 only: padded attention needs `batch × heads × seq²`, which at this
150
+ corpus's 4,746-token maximum is about 17 GB of attention scores at batch 32. For comparison, the
151
+ pre-dispatch revision at batch 32 peaks at 4,456 MiB and takes 23.8 s (3090 Ti) and 21.7 s
152
+ (5090 Laptop).
153
+
154
+
155
+ | GPU | load | batch size | tier | mean cos | min cos | encode peak | wall |
156
+ | --- | --- | --- | --- | --- | --- | --- | --- |
157
+ | RTX 3090 Ti (sm_86) | default | 4 | `eager` | 0.999923 | 0.999404 | 4,321 MiB | 41.791 s |
158
+ | RTX 3090 Ti (sm_86) | default | 32 | `torch_varlen` | 0.999948 | 0.999403 | 2,511 MiB | 16.327 s |
159
+ | RTX 3090 Ti (sm_86) | default | 32 | `flash_attn` | 0.999974 | 0.999814 | 2,511 MiB | 16.330 s |
160
+ | RTX 3090 Ti (sm_86) | default | 256 | `torch_varlen` | 0.999949 | 0.999403 | 11,135 MiB | 15.027 s |
161
+ | RTX 3090 Ti (sm_86) | default | 256 | `flash_attn` | 0.999979 | 0.999776 | 11,136 MiB | 15.056 s |
162
+ | RTX 3090 Ti (sm_86) | default | 1,024 | `torch_varlen` | out of memory | | | |
163
+ | RTX 3090 Ti (sm_86) | default | 1,024 | `flash_attn` | out of memory | | | |
164
+ | RTX 3090 Ti (sm_86) | default | 2,048 | `torch_varlen` | out of memory | | | |
165
+ | RTX 3090 Ti (sm_86) | default | 2,048 | `flash_attn` | out of memory | | | |
166
+ | RTX 3090 Ti (sm_86) | flattened | 4 | `eager` | 0.999923 | 0.999404 | 4,329 MiB | 51.793 s |
167
+ | RTX 3090 Ti (sm_86) | flattened | 32 | `torch_varlen` | 0.999948 | 0.999403 | 1,468 MiB | 15.529 s |
168
+ | RTX 3090 Ti (sm_86) | flattened | 32 | `flash_attn` | 0.999967 | 0.999776 | 1,466 MiB | 15.496 s |
169
+ | RTX 3090 Ti (sm_86) | flattened | 256 | `torch_varlen` | 0.999949 | 0.999403 | 7,073 MiB | 13.622 s |
170
+ | RTX 3090 Ti (sm_86) | flattened | 256 | `flash_attn` | 0.999978 | 0.999776 | 7,072 MiB | 13.641 s |
171
+ | RTX 3090 Ti (sm_86) | flattened | 1,024 | `torch_varlen` | 0.999949 | 0.999403 | 17,411 MiB | 13.371 s |
172
+ | RTX 3090 Ti (sm_86) | flattened | 1,024 | `flash_attn` | 0.999978 | 0.999776 | 17,412 MiB | 13.478 s |
173
+ | RTX 3090 Ti (sm_86) | flattened | 2,048 | `torch_varlen` | out of memory | | | |
174
+ | RTX 3090 Ti (sm_86) | flattened | 2,048 | `flash_attn` | out of memory | | | |
175
+ | RTX 5090 Laptop GPU (sm_120) | default | 4 | `eager` | 0.999925 | 0.999721 | 4,321 MiB | 33.589 s |
176
+ | RTX 5090 Laptop GPU (sm_120) | default | 32 | `torch_varlen` | 0.999949 | 0.999748 | 2,511 MiB | 14.391 s |
177
+ | RTX 5090 Laptop GPU (sm_120) | default | 32 | `flash_attn` | 0.999976 | 0.999766 | 2,511 MiB | 14.423 s |
178
+ | RTX 5090 Laptop GPU (sm_120) | default | 256 | `torch_varlen` | 0.999951 | 0.999748 | 11,134 MiB | 15.171 s |
179
+ | RTX 5090 Laptop GPU (sm_120) | default | 256 | `flash_attn` | 0.999988 | 0.999811 | 11,136 MiB | 15.155 s |
180
+ | RTX 5090 Laptop GPU (sm_120) | default | 1,024 | `torch_varlen` | out of memory | | | |
181
+ | RTX 5090 Laptop GPU (sm_120) | default | 1,024 | `flash_attn` | out of memory | | | |
182
+ | RTX 5090 Laptop GPU (sm_120) | default | 2,048 | `torch_varlen` | out of memory | | | |
183
+ | RTX 5090 Laptop GPU (sm_120) | default | 2,048 | `flash_attn` | out of memory | | | |
184
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 4 | `eager` | 0.999924 | 0.998160 | 4,329 MiB | 44.613 s |
185
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 32 | `torch_varlen` | 0.999950 | 0.999748 | 1,468 MiB | 13.699 s |
186
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 32 | `flash_attn` | 0.999985 | 0.999811 | 1,466 MiB | 13.758 s |
187
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 256 | `torch_varlen` | 0.999951 | 0.999748 | 7,072 MiB | 14.402 s |
188
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 256 | `flash_attn` | 0.999988 | 0.999811 | 7,072 MiB | 14.478 s |
189
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 1,024 | `torch_varlen` | 0.999951 | 0.999748 | 17,412 MiB | 14.739 s |
190
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 1,024 | `flash_attn` | 0.999988 | 0.999811 | 17,412 MiB | 14.783 s |
191
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 2,048 | `torch_varlen` | out of memory | | | |
192
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 2,048 | `flash_attn` | out of memory | | | |
193
 
194
  Cosines rounded to 6 decimal places, VRAM to the nearest MiB. `torch 2.12.1+cu130`,
195
  `transformers 5.11.0`, `sentence-transformers 6.1.0`, `flash-attn 2.8.3` (`2.8.3.post1` on the
196
+ 3090 Ti).
197
 
198
+ Memory tracks the real tokens in the heaviest batch, not the number of functions: both
199
+ loads run the encoder padding-free, and each out-of-memory row failed allocating one 3,072-wide
200
+ MLP activation over exactly that batch's tokens. sentence-transformers length-sorts the corpus,
201
+ so under the default load the first batch of 1,024 gathers the longest functions, 869,948 tokens,
202
+ which does not fit on 24 GB. When it flattens, it interleaves longest with shortest to even out
203
+ batches, so its heaviest batch of 1,024 holds 593,716 tokens and fits at 17.4 GB; at 2,048 even
204
+ that reaches 889,626 tokens and does not. Up to that limit, flattening cuts encode memory by
205
+ 36–42% and wall time by 5–9% against the default load. The interleaving
206
+ also changes which texts share a padded `eager` batch, which is why the flattened `eager` row's
207
+ minimum differs on the 5090 Laptop: its lowest text is a 2,166-token function padded next to a
208
+ 2,195-token one (the same text alone scores 0.999936).
209
 
210
  **Real documents at the model defaults.** 768 Python files sampled with a fixed seed from one shard
211
  of [`HuggingFaceTB/stack-edu`](https://huggingface.co/datasets/HuggingFaceTB/stack-edu) (730,431
212
  tokens after truncation), encoded with sentence-transformers at this model's default
213
+ `max_seq_length` of 8192, under the default load and under the flattened opt-in. **Sorted** is
214
+ one `encode()` call over the whole corpus, so sentence-transformers length-sorts the files before
215
+ batching; **file** is one `encode()` call per batch of consecutive files, so each batch mixes short
216
+ and long files. Wall time covers the whole corpus; peak VRAM is the peak CUDA allocation, weights
217
+ included. The largest sorted batch on each card is the largest batch size that fit (the 5090
218
+ Laptop was sharing about 0.4 GiB with another process).
219
+
220
+ | GPU | load | order | batch size | tier | wall (768 files) | peak VRAM |
221
+ | --- | --- | --- | --- | --- | --- | --- |
222
+ | RTX 3090 Ti (sm_86) | default | sorted | 4 | `torch_varlen` | 5.61 s | 1,296 MiB |
223
+ | RTX 3090 Ti (sm_86) | default | sorted | 32 | `torch_varlen` | 4.79 s | 6,186 MiB |
224
+ | RTX 3090 Ti (sm_86) | default | sorted | 64 | `torch_varlen` | 4.80 s | 9,253 MiB |
225
+ | RTX 3090 Ti (sm_86) | default | sorted | 353 | `torch_varlen` | 5.77 s | 18,637 MiB |
226
+ | RTX 3090 Ti (sm_86) | default | sorted | 32 | `flash_attn` | 4.79 s | 6,187 MiB |
227
+ | RTX 3090 Ti (sm_86) | default | file | 4 | `torch_varlen` | 6.49 s | 636 MiB |
228
+ | RTX 3090 Ti (sm_86) | default | file | 32 | `torch_varlen` | 6.39 s | 1,614 MiB |
229
+ | RTX 3090 Ti (sm_86) | default | file | 64 | `torch_varlen` | 6.44 s | 2,801 MiB |
230
+ | RTX 3090 Ti (sm_86) | flattened | sorted | 4 | `torch_varlen` | 5.51 s | 792 MiB |
231
+ | RTX 3090 Ti (sm_86) | flattened | sorted | 32 | `torch_varlen` | 4.54 s | 3,843 MiB |
232
+ | RTX 3090 Ti (sm_86) | flattened | sorted | 64 | `torch_varlen` | 4.47 s | 6,237 MiB |
233
+ | RTX 3090 Ti (sm_86) | flattened | sorted | 628 | `torch_varlen` | 4.60 s | 19,658 MiB |
234
+ | RTX 3090 Ti (sm_86) | flattened | sorted | 32 | `flash_attn` | 4.54 s | 3,844 MiB |
235
+ | RTX 3090 Ti (sm_86) | flattened | file | 4 | `torch_varlen` | 5.83 s | 634 MiB |
236
+ | RTX 3090 Ti (sm_86) | flattened | file | 32 | `torch_varlen` | 4.73 s | 1,607 MiB |
237
+ | RTX 3090 Ti (sm_86) | flattened | file | 64 | `torch_varlen` | 4.53 s | 2,787 MiB |
238
+ | RTX 5090 Laptop GPU (sm_120) | default | sorted | 4 | `torch_varlen` | 4.75 s | 1,319 MiB |
239
+ | RTX 5090 Laptop GPU (sm_120) | default | sorted | 32 | `torch_varlen` | 4.72 s | 6,209 MiB |
240
+ | RTX 5090 Laptop GPU (sm_120) | default | sorted | 64 | `torch_varlen` | 4.84 s | 9,276 MiB |
241
+ | RTX 5090 Laptop GPU (sm_120) | default | sorted | 353 | `torch_varlen` | 5.98 s | 18,661 MiB |
242
+ | RTX 5090 Laptop GPU (sm_120) | default | sorted | 32 | `flash_attn` | 4.71 s | 6,210 MiB |
243
+ | RTX 5090 Laptop GPU (sm_120) | default | file | 4 | `torch_varlen` | 5.64 s | 660 MiB |
244
+ | RTX 5090 Laptop GPU (sm_120) | default | file | 32 | `torch_varlen` | 5.70 s | 1,636 MiB |
245
+ | RTX 5090 Laptop GPU (sm_120) | default | file | 64 | `torch_varlen` | 5.84 s | 2,823 MiB |
246
+ | RTX 5090 Laptop GPU (sm_120) | flattened | sorted | 4 | `torch_varlen` | 4.56 s | 816 MiB |
247
+ | RTX 5090 Laptop GPU (sm_120) | flattened | sorted | 32 | `torch_varlen` | 4.56 s | 3,866 MiB |
248
+ | RTX 5090 Laptop GPU (sm_120) | flattened | sorted | 64 | `torch_varlen` | 4.63 s | 6,260 MiB |
249
+ | RTX 5090 Laptop GPU (sm_120) | flattened | sorted | 628 | `torch_varlen` | 4.75 s | 19,681 MiB |
250
+ | RTX 5090 Laptop GPU (sm_120) | flattened | sorted | 32 | `flash_attn` | 4.57 s | 3,867 MiB |
251
+ | RTX 5090 Laptop GPU (sm_120) | flattened | file | 4 | `torch_varlen` | 4.75 s | 658 MiB |
252
+ | RTX 5090 Laptop GPU (sm_120) | flattened | file | 32 | `torch_varlen` | 4.70 s | 1,630 MiB |
253
+ | RTX 5090 Laptop GPU (sm_120) | flattened | file | 64 | `torch_varlen` | 4.66 s | 2,811 MiB |
254
+
255
+ Flattening keeps peak memory well below the default load at every sorted batch size and fits a
256
+ batch of 628 on 24 GB, where the default load fits 353. It also makes a text's embedding
257
+ independent of the other texts in its batch: between the file-order and sorted runs, the minimum
258
+ cosine over the 768 files is 0.9999998 on both cards and both varlen tiers under the flattened
259
+ load, against as low as 0.99987 under the default load.
260
+
261
+ ### torch.compile
262
+
263
+ The parity table's 16,384 functions on `torch_varlen`, with the encoder layers eager and compiled
264
+ (`torch.compile(model[0].auto_model.encoder, dynamic=True)`). Cosine is against the same
265
+ pre-dispatch encode as the parity table.
266
+
267
+ | GPU | load | batch size | eager | compiled | compiled min cos (mean) |
268
+ | --- | --- | --- | --- | --- | --- |
269
+ | RTX 3090 Ti (sm_86) | default | 32 | 16.38 s, 2,511 MiB | 14.05 s, 1,635 MiB | 0.998156 (0.999943) |
270
+ | RTX 3090 Ti (sm_86) | default | 256 | 15.06 s, 11,135 MiB | 12.74 s, 7,249 MiB | 0.998156 (0.999943) |
271
+ | RTX 3090 Ti (sm_86) | default | 1,024 | out of memory | 13.81 s, 16,701 MiB | 0.998156 (0.999943) |
272
+ | RTX 3090 Ti (sm_86) | flattened | 32 | 15.58 s, 1,468 MiB | 13.16 s, 956 MiB | 0.998156 (0.999943) |
273
+ | RTX 3090 Ti (sm_86) | flattened | 256 | 13.67 s, 7,073 MiB | 11.37 s, 4,602 MiB | 0.998156 (0.999943) |
274
+ | RTX 3090 Ti (sm_86) | flattened | 1,024 | 13.40 s, 17,411 MiB | 10.90 s, 11,325 MiB | 0.998156 (0.999943) |
275
+ | RTX 5090 Laptop GPU (sm_120) | default | 32 | 14.33 s, 2,511 MiB | 12.19 s, 1,637 MiB | 0.998657 (0.999944) |
276
+ | RTX 5090 Laptop GPU (sm_120) | default | 256 | 15.08 s, 11,134 MiB | 12.30 s, 7,249 MiB | 0.998657 (0.999944) |
277
+ | RTX 5090 Laptop GPU (sm_120) | default | 1,024 | out of memory | 13.39 s, 16,701 MiB | 0.999250 (0.999944) |
278
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 32 | 13.67 s, 1,468 MiB | 11.72 s, 956 MiB | 0.999250 (0.999944) |
279
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 256 | 14.35 s, 7,072 MiB | 11.66 s, 4,601 MiB | 0.999250 (0.999944) |
280
+ | RTX 5090 Laptop GPU (sm_120) | flattened | 1,024 | 14.67 s, 17,412 MiB | 11.80 s, 11,326 MiB | 0.999250 (0.999944) |
281
+
282
+ Compiling makes the encode 1.17–1.24× faster and cuts its peak memory by about a third, because
283
+ inductor fuses the element-wise work around each attention call; the default load's batch of 1,024
284
+ then fits. It compiles one graph with no graph breaks, once per process (the first encode took about
285
+ 4 s longer on the 5090 Laptop and 10 s on the 3090 Ti), and does not recompile as batch lengths
286
+ change. The one further compile comes once a batch passes about 699,000 tokens, where inductor
287
+ switches to 64-bit indexing. The mean cosine is unchanged; a few texts move by bf16 reassociation,
288
+ to a minimum of 0.998156 against 0.999403 eager on the 3090 Ti.
289
 
290
  ## What changed vs the source repo
291
 
 
304
  builds that have it installed), and `eager` (the original padded attention algorithm, numerically
305
  unchanged, and the default off CUDA sm_80+; it now converts a raw 2-D `[B, S]` mask to the
306
  additive form before adding it, for the case where a `device_map` split hands it a mask a varlen
307
+ tier upstream never passes). On both varlen tiers `NomicBertModel.forward` unpads the batch once
308
+ with torch-native `_unpad`/`_pad` helpers (a replacement for `flash_attn.bert_padding`), keeps
309
+ the hidden states packed `[ΣL, 768]` through the embeddings and every layer, and pads back once at
310
+ the end. Rotary embeddings rotate each packed token by its position within its own sequence,
311
+ the same rotation it gets in the padded batch. On the eager tier `NomicBertModel.forward` builds
312
+ the additive attention mask inline instead of calling the (now-removed-upstream)
313
+ `get_extended_attention_mask` helper. Set `NOMIC_BERT_ATTN_IMPL=torch_varlen|flash_attn|eager`
314
+ to force a tier (raises if it can't engage); `model[0].auto_model.attention_impl` and the
315
+ one-time INFO log line report which tier engaged.
316
  4. **`from_pretrained` forwards the Hub revision**: the upstream custom `from_pretrained` fetched
317
  the weight file without the caller's `revision` (and cache/token options), so a pinned load
318
  still took the weights from the default branch. The copy here passes them through, so a
319
  pinned load fetches the weights from the pinned commit too. It also accepts transformers'
320
+ `dtype=` alongside the older `torch_dtype=`, and returns the model in eval mode, as
321
+ transformers' own `from_pretrained` does.
322
+ 5. **Flattened input**: `NomicBertModel.forward` also accepts sentence-transformers' flattened
323
+ batches (`input_ids [1, ΣL]` with per-sequence `position_ids` and `cu_seq_lens_q`), and the
324
+ model declares flash-attention support so that sentence-transformers sends them when loaded
325
+ with `attn_implementation="flash_attention_2"` (see Behavior).
326
 
327
  ## License & attribution
328
 
modeling_hf_nomic_bert.py CHANGED
@@ -12,7 +12,7 @@ import os
12
  import re
13
  from collections import OrderedDict
14
  from functools import partial
15
- from typing import List, Optional, Tuple, Union
16
 
17
  import torch
18
  import torch.nn as nn
@@ -236,6 +236,21 @@ def _pad(x_u: torch.Tensor, indices: torch.Tensor, B: int, S: int) -> torch.Tens
236
  return out.view(B, S, *x_u.shape[1:])
237
 
238
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  def select_attention_impl(device: torch.device) -> str:
240
  """The attention tier for ``device`` under the current ``NOMIC_BERT_ATTN_IMPL``.
241
 
@@ -541,6 +556,10 @@ class NomicBertPreTrainedModel(PreTrainedModel):
541
  supports_gradient_checkpointing = True
542
  _no_split_modules = ["Block"]
543
  _skip_keys_device_placement = "past_key_values"
 
 
 
 
544
 
545
  def __init__(self, config, *inputs, **kwargs):
546
  super().__init__(config)
@@ -598,6 +617,9 @@ class NomicBertPreTrainedModel(PreTrainedModel):
598
  if rotary_scaling_factor:
599
  config.rotary_scaling_factor = rotary_scaling_factor
600
 
 
 
 
601
  if config.n_positions <= 0 and config.rotary_emb_fraction > 0:
602
  config.n_positions = 2048
603
  if num_labels:
@@ -663,6 +685,9 @@ class NomicBertPreTrainedModel(PreTrainedModel):
663
  logger.warning(load_return)
664
  else:
665
  logger.debug(load_return)
 
 
 
666
  return model
667
 
668
  def _set_gradient_checkpointing(self, module, value=False):
@@ -929,6 +954,29 @@ class NomicBertRotaryEmbedding(nn.Module):
929
  return torch.stack((q_rot, k_rot, qkv[:, :, 2]), dim=2)
930
 
931
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
932
  class NomicBertDynamicNTKRotaryEmbedding(NomicBertRotaryEmbedding):
933
  def __init__(self, rotary_scaling_factor, max_position_embeddings, **kwargs):
934
  super().__init__(**kwargs)
@@ -1069,8 +1117,12 @@ class NomicBertAttention(nn.Module):
1069
  is_padded_inputs: Optional[bool] = True,
1070
  cu_seqlens: Optional[torch.Tensor] = None,
1071
  max_seq_len: Optional[int] = None,
 
1072
  ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
1073
 
 
 
 
1074
  has_layer_past = past_key_value is not None
1075
 
1076
  if has_layer_past:
@@ -1172,6 +1224,23 @@ class NomicBertAttention(nn.Module):
1172
 
1173
  return attn_output
1174
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1175
 
1176
  class NomicBertBlock(nn.Module):
1177
  def __init__(
@@ -1225,6 +1294,7 @@ class NomicBertBlock(nn.Module):
1225
  use_cache: Optional[bool] = False,
1226
  cu_seqlens: Optional[torch.Tensor] = None,
1227
  max_seq_len: Optional[int] = None,
 
1228
  ):
1229
  r"""Pass the input through the encoder layer.
1230
  Args:
@@ -1244,6 +1314,7 @@ class NomicBertBlock(nn.Module):
1244
  is_padded_inputs=is_padded_inputs,
1245
  cu_seqlens=cu_seqlens,
1246
  max_seq_len=max_seq_len,
 
1247
  )
1248
 
1249
  dropped = self.dropout2(hidden_states)
@@ -1260,6 +1331,7 @@ class NomicBertBlock(nn.Module):
1260
  is_padded_inputs=is_padded_inputs,
1261
  cu_seqlens=cu_seqlens,
1262
  max_seq_len=max_seq_len,
 
1263
  )
1264
  hidden_states = self.norm1((self.dropout1(attn_outputs) + hidden_states).to(dtype=self.norm1.weight.dtype))
1265
  mlp_out = self.mlp(hidden_states)
@@ -1287,6 +1359,7 @@ class NomicBertEncoder(nn.Module):
1287
  output_hidden_states: Optional[bool] = None,
1288
  return_dict: Optional[bool] = None,
1289
  is_padded_inputs: Optional[bool] = True,
 
1290
  ):
1291
  """If subset_mask is not None, we only want output for the subset of the sequence.
1292
  This means that we only compute the last layer output for these tokens.
@@ -1314,6 +1387,7 @@ class NomicBertEncoder(nn.Module):
1314
  None,
1315
  None,
1316
  is_padded_inputs,
 
1317
  # if you freeze ANY layers, you need `use_reentrant=False`
1318
  # https://github.com/huggingface/transformers/issues/21381
1319
  # https://discuss.pytorch.org/t/checkpoint-with-no-grad-requiring-inputs-problem/19117/7
@@ -1331,6 +1405,7 @@ class NomicBertEncoder(nn.Module):
1331
  is_padded_inputs,
1332
  output_attentions,
1333
  use_cache,
 
1334
  )
1335
  return hidden_states
1336
 
@@ -1427,16 +1502,26 @@ class NomicBertModel(NomicBertPreTrainedModel):
1427
  token_type_ids=None,
1428
  position_ids=None,
1429
  return_dict=None,
 
 
 
1430
  ):
1431
  if token_type_ids is None:
1432
  token_type_ids = torch.zeros_like(input_ids)
 
 
 
 
 
 
 
 
 
 
 
1433
  hidden_states = self.embeddings(input_ids, position_ids=position_ids, token_type_ids=token_type_ids)
1434
  hidden_states = self.emb_ln(hidden_states)
1435
  hidden_states = self.emb_drop(hidden_states)
1436
-
1437
- impl = select_attention_impl(hidden_states.device)
1438
- self.attention_impl = impl
1439
- _log_attention_impl_once(impl, hidden_states.device)
1440
  if impl == "eager":
1441
  # No varlen tier engaged (CPU, pre-Ampere, forced, or no kernel):
1442
  # build the additive [B, 1, 1, S] mask inline for the eager block.
@@ -1454,6 +1539,94 @@ class NomicBertModel(NomicBertPreTrainedModel):
1454
  pooler_output=pooled_output,
1455
  )
1456
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1457
 
1458
  class NomicBertForPreTraining(NomicBertPreTrainedModel):
1459
  _tied_weights_keys = ["predictions.decoder.bias", "cls.predictions.decoder.weight"]
 
12
  import re
13
  from collections import OrderedDict
14
  from functools import partial
15
+ from typing import NamedTuple, List, Optional, Tuple, Union
16
 
17
  import torch
18
  import torch.nn as nn
 
236
  return out.view(B, S, *x_u.shape[1:])
237
 
238
 
239
+ class _Packed(NamedTuple):
240
+ """Metadata for hidden states kept packed ``[N, D]`` across the encoder.
241
+
242
+ ``cu_seqlens`` / ``max_seqlen`` feed the varlen kernel; ``positions`` (int64,
243
+ ``[N]``) is each token's column in the padded batch, the position RoPE uses on
244
+ the dense path; ``rope_seqlen`` is the padded width ``S``, the length the dense
245
+ path sizes the RoPE cache to.
246
+ """
247
+
248
+ cu_seqlens: torch.Tensor
249
+ max_seqlen: int
250
+ positions: torch.Tensor
251
+ rope_seqlen: int
252
+
253
+
254
  def select_attention_impl(device: torch.device) -> str:
255
  """The attention tier for ``device`` under the current ``NOMIC_BERT_ATTN_IMPL``.
256
 
 
556
  supports_gradient_checkpointing = True
557
  _no_split_modules = ["Block"]
558
  _skip_keys_device_placement = "past_key_values"
559
+ # Pre-packed input (cu_seq_lens_q / position_ids through forward's **kwargs) runs on
560
+ # either varlen tier, so the model can accept a flattened batch.
561
+ _supports_attention_backend = True
562
+ _supports_flash_attn = True
563
 
564
  def __init__(self, config, *inputs, **kwargs):
565
  super().__init__(config)
 
617
  if rotary_scaling_factor:
618
  config.rotary_scaling_factor = rotary_scaling_factor
619
 
620
+ if kwargs.get("attn_implementation") == "flash_attention_2":
621
+ # Opt-in: advertises flash attention so sentence-transformers flattens batches.
622
+ config._attn_implementation = "flash_attention_2"
623
  if config.n_positions <= 0 and config.rotary_emb_fraction > 0:
624
  config.n_positions = 2048
625
  if num_labels:
 
685
  logger.warning(load_return)
686
  else:
687
  logger.debug(load_return)
688
+ # transformers' native from_pretrained returns the model in eval mode; this
689
+ # override must too, or a direct forward runs the config's dropout.
690
+ model.eval()
691
  return model
692
 
693
  def _set_gradient_checkpointing(self, module, value=False):
 
954
  return torch.stack((q_rot, k_rot, qkv[:, :, 2]), dim=2)
955
 
956
 
957
+ def _rotary_packed(rotary: "NomicBertRotaryEmbedding", qkv_u: torch.Tensor, packed: _Packed) -> torch.Tensor:
958
+ """RoPE on packed ``[N, 3, H, D]``: each token rotated by its own column position.
959
+
960
+ Same arithmetic as ``NomicBertRotaryEmbedding.forward`` on the dense
961
+ ``[B, S, 3, H, D]`` tensor, so every real token gets bit-identical q/k. The caller
962
+ sizes the cache first (``NomicBertModel._size_rotary_caches``), outside the encoder:
963
+ the update branches on the module's cached length, which ``torch.compile`` guards as
964
+ a static int, so updating it here would recompile a compiled encoder on every growth.
965
+ """
966
+ pattern = "... d -> ... 1 (2 d)" if not rotary.interleaved else "... d -> ... 1 (d 2)"
967
+ cos = repeat(rotary._cos_cached[packed.positions], pattern)
968
+ sin = repeat(rotary._sin_cached[packed.positions], pattern)
969
+ ro_dim = cos.shape[-1]
970
+
971
+ def rot(x):
972
+ return torch.cat(
973
+ [x[..., :ro_dim] * cos + rotate_half(x[..., :ro_dim], rotary.interleaved) * sin, x[..., ro_dim:]],
974
+ dim=-1,
975
+ )
976
+
977
+ return torch.stack((rot(qkv_u[:, 0]), rot(qkv_u[:, 1]), qkv_u[:, 2]), dim=1)
978
+
979
+
980
  class NomicBertDynamicNTKRotaryEmbedding(NomicBertRotaryEmbedding):
981
  def __init__(self, rotary_scaling_factor, max_position_embeddings, **kwargs):
982
  super().__init__(**kwargs)
 
1117
  is_padded_inputs: Optional[bool] = True,
1118
  cu_seqlens: Optional[torch.Tensor] = None,
1119
  max_seq_len: Optional[int] = None,
1120
+ packed: Optional[_Packed] = None,
1121
  ) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[Tuple[torch.Tensor]]]:
1122
 
1123
+ if packed is not None:
1124
+ return self._forward_packed(hidden_states, packed)
1125
+
1126
  has_layer_past = past_key_value is not None
1127
 
1128
  if has_layer_past:
 
1224
 
1225
  return attn_output
1226
 
1227
+ def _forward_packed(self, hidden_states: torch.Tensor, packed: _Packed) -> torch.Tensor:
1228
+ """Varlen attention on packed ``[N, D]`` hidden states; returns ``[N, D]``."""
1229
+ qkv = self.Wqkv(hidden_states)
1230
+ qkv = rearrange(qkv, "n (three h d) -> n three h d", three=3, d=self.head_dim)
1231
+ if self.rotary_emb_dim > 0:
1232
+ qkv = _rotary_packed(self.rotary_emb, qkv, packed)
1233
+ orig_dtype = qkv.dtype
1234
+ if orig_dtype not in (torch.float16, torch.bfloat16):
1235
+ qkv = qkv.to(torch.bfloat16)
1236
+ cu, max_s = packed.cu_seqlens, packed.max_seqlen
1237
+ if select_attention_impl(hidden_states.device) == "torch_varlen":
1238
+ q, k, v = qkv.unbind(1)
1239
+ out = _torch_varlen_attn(q, k, v, cu, cu, max_s, max_s)
1240
+ else:
1241
+ out = flash_attn_varlen_qkvpacked_func(qkv, cu, max_s, dropout_p=0.0, causal=False)
1242
+ return self.out_proj(rearrange(out.to(orig_dtype), "n h d -> n (h d)"))
1243
+
1244
 
1245
  class NomicBertBlock(nn.Module):
1246
  def __init__(
 
1294
  use_cache: Optional[bool] = False,
1295
  cu_seqlens: Optional[torch.Tensor] = None,
1296
  max_seq_len: Optional[int] = None,
1297
+ packed: Optional[_Packed] = None,
1298
  ):
1299
  r"""Pass the input through the encoder layer.
1300
  Args:
 
1314
  is_padded_inputs=is_padded_inputs,
1315
  cu_seqlens=cu_seqlens,
1316
  max_seq_len=max_seq_len,
1317
+ packed=packed,
1318
  )
1319
 
1320
  dropped = self.dropout2(hidden_states)
 
1331
  is_padded_inputs=is_padded_inputs,
1332
  cu_seqlens=cu_seqlens,
1333
  max_seq_len=max_seq_len,
1334
+ packed=packed,
1335
  )
1336
  hidden_states = self.norm1((self.dropout1(attn_outputs) + hidden_states).to(dtype=self.norm1.weight.dtype))
1337
  mlp_out = self.mlp(hidden_states)
 
1359
  output_hidden_states: Optional[bool] = None,
1360
  return_dict: Optional[bool] = None,
1361
  is_padded_inputs: Optional[bool] = True,
1362
+ packed: Optional[_Packed] = None,
1363
  ):
1364
  """If subset_mask is not None, we only want output for the subset of the sequence.
1365
  This means that we only compute the last layer output for these tokens.
 
1387
  None,
1388
  None,
1389
  is_padded_inputs,
1390
+ packed=packed,
1391
  # if you freeze ANY layers, you need `use_reentrant=False`
1392
  # https://github.com/huggingface/transformers/issues/21381
1393
  # https://discuss.pytorch.org/t/checkpoint-with-no-grad-requiring-inputs-problem/19117/7
 
1405
  is_padded_inputs,
1406
  output_attentions,
1407
  use_cache,
1408
+ packed=packed,
1409
  )
1410
  return hidden_states
1411
 
 
1502
  token_type_ids=None,
1503
  position_ids=None,
1504
  return_dict=None,
1505
+ cu_seq_lens_q=None,
1506
+ max_length_q=None,
1507
+ **kwargs,
1508
  ):
1509
  if token_type_ids is None:
1510
  token_type_ids = torch.zeros_like(input_ids)
1511
+ if cu_seq_lens_q is not None:
1512
+ self.attention_impl = select_attention_impl(input_ids.device)
1513
+ _log_attention_impl_once(self.attention_impl, input_ids.device)
1514
+ return self._forward_prepacked(input_ids, token_type_ids, position_ids, cu_seq_lens_q, max_length_q)
1515
+
1516
+ impl = select_attention_impl(input_ids.device)
1517
+ self.attention_impl = impl
1518
+ _log_attention_impl_once(impl, input_ids.device)
1519
+ if impl != "eager" and (attention_mask is None or attention_mask.ndim == 2):
1520
+ return self._forward_packed(input_ids, attention_mask, token_type_ids, position_ids)
1521
+
1522
  hidden_states = self.embeddings(input_ids, position_ids=position_ids, token_type_ids=token_type_ids)
1523
  hidden_states = self.emb_ln(hidden_states)
1524
  hidden_states = self.emb_drop(hidden_states)
 
 
 
 
1525
  if impl == "eager":
1526
  # No varlen tier engaged (CPU, pre-Ampere, forced, or no kernel):
1527
  # build the additive [B, 1, 1, S] mask inline for the eager block.
 
1539
  pooler_output=pooled_output,
1540
  )
1541
 
1542
+ def _size_rotary_caches(self, seqlen, device):
1543
+ """Grow each layer's RoPE cos/sin cache to ``seqlen`` before the packed encoder runs
1544
+ (see ``_rotary_packed``), in the dtype its q/k projection produces."""
1545
+ for layer in self.encoder.layers:
1546
+ attn = layer.attn
1547
+ if attn.rotary_emb_dim > 0:
1548
+ attn.rotary_emb._update_cos_sin_cache(seqlen, device=device, dtype=attn.Wqkv.weight.dtype)
1549
+
1550
+ def _forward_packed(self, input_ids, attention_mask, token_type_ids, position_ids):
1551
+ """Varlen tiers: unpad once, run embeddings and every layer on real tokens only.
1552
+
1553
+ ``last_hidden_state`` comes back padded ``[B, S, D]`` with zeros at padding
1554
+ positions, so masked pooling downstream is unchanged.
1555
+ """
1556
+ B, S = input_ids.shape
1557
+ mask = (
1558
+ torch.ones(B, S, device=input_ids.device, dtype=torch.bool)
1559
+ if attention_mask is None
1560
+ else attention_mask.to(torch.bool)
1561
+ )
1562
+ seqlens = mask.sum(-1, dtype=torch.int32)
1563
+ max_s = int(seqlens.max())
1564
+ if max_s == 0:
1565
+ raise RuntimeError(
1566
+ "NomicBertModel.forward: every sequence is empty (max_s == 0); "
1567
+ "pre-filter empty inputs before encoding."
1568
+ )
1569
+ indices = torch.nonzero(mask.flatten(), as_tuple=False).flatten()
1570
+ cu = F.pad(torch.cumsum(seqlens, 0, dtype=torch.int32), (1, 0))
1571
+ columns = indices % S
1572
+ pos_u = position_ids.flatten()[indices] if position_ids is not None else columns
1573
+ hidden_states = self.embeddings(
1574
+ input_ids.flatten()[indices][None],
1575
+ position_ids=pos_u[None],
1576
+ token_type_ids=token_type_ids.flatten()[indices][None],
1577
+ )[0]
1578
+ hidden_states = self.emb_drop(self.emb_ln(hidden_states))
1579
+ packed = _Packed(cu_seqlens=cu, max_seqlen=max_s, positions=columns, rope_seqlen=S)
1580
+ self._size_rotary_caches(packed.rope_seqlen, hidden_states.device)
1581
+ hidden_states = self.encoder(hidden_states, packed=packed)
1582
+ sequence_output = _pad(hidden_states, indices, B, S)
1583
+ pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
1584
+ return BaseModelOutputWithPoolingAndCrossAttentions(
1585
+ last_hidden_state=sequence_output,
1586
+ pooler_output=pooled_output,
1587
+ )
1588
+
1589
+ def _forward_prepacked(self, input_ids, token_type_ids, position_ids, cu_seq_lens_q, max_length_q):
1590
+ """Input already packed by the caller: ``input_ids [1, ΣL]`` with per-sequence
1591
+ ``position_ids`` and ``cu_seq_lens_q`` boundaries. Returns ``[1, ΣL, D]``."""
1592
+ if self.attention_impl == "eager":
1593
+ return self._forward_prepacked_eager(input_ids, token_type_ids, cu_seq_lens_q)
1594
+ if position_ids is None:
1595
+ raise ValueError("NomicBertModel.forward: pre-packed input needs per-sequence position_ids")
1596
+ positions = position_ids.reshape(-1).long()
1597
+ max_s = int(max_length_q) if max_length_q is not None else int((cu_seq_lens_q[1:] - cu_seq_lens_q[:-1]).max())
1598
+ hidden_states = self.embeddings(input_ids, position_ids=position_ids, token_type_ids=token_type_ids)[0]
1599
+ hidden_states = self.emb_drop(self.emb_ln(hidden_states))
1600
+ packed = _Packed(cu_seqlens=cu_seq_lens_q.to(torch.int32), max_seqlen=max_s, positions=positions,
1601
+ rope_seqlen=max_s)
1602
+ self._size_rotary_caches(packed.rope_seqlen, hidden_states.device)
1603
+ sequence_output = self.encoder(hidden_states, packed=packed)[None]
1604
+ pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
1605
+ return BaseModelOutputWithPoolingAndCrossAttentions(
1606
+ last_hidden_state=sequence_output,
1607
+ pooler_output=pooled_output,
1608
+ )
1609
+
1610
+ def _forward_prepacked_eager(self, input_ids, token_type_ids, cu_seq_lens_q):
1611
+ """Pre-packed input on the eager tier (forced, or no varlen kernel on this device):
1612
+ re-pad to ``[B, S]`` (right padding, so each token keeps its per-sequence
1613
+ position), run the padded eager forward, and gather the real tokens back to
1614
+ ``[1, ΣL, D]``, the shape the caller's pooling expects."""
1615
+ cu = cu_seq_lens_q.long()
1616
+ seqlens = cu[1:] - cu[:-1]
1617
+ B, S = seqlens.shape[0], int(seqlens.max())
1618
+ mask = torch.arange(S, device=input_ids.device)[None, :] < seqlens[:, None]
1619
+ indices = torch.nonzero(mask.flatten(), as_tuple=False).flatten()
1620
+ ids = _pad(input_ids.reshape(-1), indices, B, S)
1621
+ types = _pad(token_type_ids.reshape(-1), indices, B, S)
1622
+ out = self.forward(ids, attention_mask=mask.long(), token_type_ids=types)
1623
+ sequence_output = out.last_hidden_state.flatten(0, 1)[indices][None]
1624
+ pooled_output = self.pooler(sequence_output) if self.pooler is not None else None
1625
+ return BaseModelOutputWithPoolingAndCrossAttentions(
1626
+ last_hidden_state=sequence_output,
1627
+ pooler_output=pooled_output,
1628
+ )
1629
+
1630
 
1631
  class NomicBertForPreTraining(NomicBertPreTrainedModel):
1632
  _tied_weights_keys = ["predictions.decoder.bias", "cls.predictions.decoder.weight"]