lewtun HF Staff commited on
Commit
5823f43
·
verified ·
1 Parent(s): d297a61

Carbon-X-SFT weights and tokenizer

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ tokenizer.json filter=lfs diff=lfs merge=lfs -text
chat_template.jinja ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {#- Training variant of the Qwen3 chat template (see qwen3.jinja for the original).
2
+ Modifications vs the original:
3
+ - Removed the loop.index0 > ns.last_query_index conditional; always include thinking block.
4
+ This makes the template prefix-preserving for the [user, assistant] → [user, assistant, tool] transition.
5
+ - Added {% generation %} / {% endgeneration %} around assistant message output to support
6
+ assistant-only loss masking in SFT training.
7
+ -#}
8
+ {%- if tools %}
9
+ {{- '<|im_start|>system\n' }}
10
+ {%- if messages[0].role == 'system' %}
11
+ {{- messages[0].content + '\n\n' }}
12
+ {%- endif %}
13
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>" }}
14
+ {%- for tool in tools %}
15
+ {{- "\n" }}
16
+ {{- tool | tojson }}
17
+ {%- endfor %}
18
+ {{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|im_end|>\n" }}
19
+ {%- else %}
20
+ {%- if messages[0].role == 'system' %}
21
+ {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
22
+ {%- endif %}
23
+ {%- endif %}
24
+ {%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
25
+ {%- for message in messages[::-1] %}
26
+ {%- set index = (messages|length - 1) - loop.index0 %}
27
+ {%- if ns.multi_step_tool and message.role == "user" and message.content is string and not(message.content.startswith('<tool_response>') and message.content.endswith('</tool_response>')) %}
28
+ {%- set ns.multi_step_tool = false %}
29
+ {%- set ns.last_query_index = index %}
30
+ {%- endif %}
31
+ {%- endfor %}
32
+ {%- for message in messages %}
33
+ {%- if message.content is string %}
34
+ {%- set content = message.content %}
35
+ {%- else %}
36
+ {%- set content = '' %}
37
+ {%- endif %}
38
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
39
+ {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>' + '\n' }}
40
+ {%- elif message.role == "assistant" %}
41
+ {%- set reasoning_content = '' %}
42
+ {%- if message.reasoning_content is string %}
43
+ {%- set reasoning_content = message.reasoning_content %}
44
+ {%- else %}
45
+ {%- if '</think>' in content %}
46
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
47
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
48
+ {%- endif %}
49
+ {%- endif %}
50
+ {{- '<|im_start|>' + message.role + '\n' }}
51
+ {%- generation %}
52
+ {{- '<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
53
+ {%- if message.tool_calls %}
54
+ {%- for tool_call in message.tool_calls %}
55
+ {%- if (loop.first and content) or (not loop.first) %}
56
+ {{- '\n' }}
57
+ {%- endif %}
58
+ {%- if tool_call.function %}
59
+ {%- set tool_call = tool_call.function %}
60
+ {%- endif %}
61
+ {{- '<tool_call>\n{"name": "' }}
62
+ {{- tool_call.name }}
63
+ {{- '", "arguments": ' }}
64
+ {%- if tool_call.arguments is string %}
65
+ {{- tool_call.arguments }}
66
+ {%- else %}
67
+ {{- tool_call.arguments | tojson }}
68
+ {%- endif %}
69
+ {{- '}\n</tool_call>' }}
70
+ {%- endfor %}
71
+ {%- endif %}
72
+ {{- '<|im_end|>\n' }}
73
+ {%- endgeneration %}
74
+ {%- elif message.role == "tool" %}
75
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
76
+ {{- '<|im_start|>user' }}
77
+ {%- endif %}
78
+ {{- '\n<tool_response>\n' }}
79
+ {{- content }}
80
+ {{- '\n</tool_response>' }}
81
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
82
+ {{- '<|im_end|>\n' }}
83
+ {%- endif %}
84
+ {%- endif %}
85
+ {%- endfor %}
86
+ {%- if add_generation_prompt %}
87
+ {{- '<|im_start|>assistant\n' }}
88
+ {%- if enable_thinking is defined and enable_thinking is false %}
89
+ {{- '<think>\n\n</think>\n\n' }}
90
+ {%- endif %}
91
+ {%- endif %}
config.json ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "Qwen3ForCausalLM"
4
+ ],
5
+ "attention_bias": false,
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": null,
8
+ "dtype": "bfloat16",
9
+ "eos_token_id": 151645,
10
+ "head_dim": 128,
11
+ "hidden_act": "silu",
12
+ "hidden_size": 5120,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 17408,
15
+ "layer_types": [
16
+ "full_attention",
17
+ "full_attention",
18
+ "full_attention",
19
+ "full_attention",
20
+ "full_attention",
21
+ "full_attention",
22
+ "full_attention",
23
+ "full_attention",
24
+ "full_attention",
25
+ "full_attention",
26
+ "full_attention",
27
+ "full_attention",
28
+ "full_attention",
29
+ "full_attention",
30
+ "full_attention",
31
+ "full_attention",
32
+ "full_attention",
33
+ "full_attention",
34
+ "full_attention",
35
+ "full_attention",
36
+ "full_attention",
37
+ "full_attention",
38
+ "full_attention",
39
+ "full_attention",
40
+ "full_attention",
41
+ "full_attention",
42
+ "full_attention",
43
+ "full_attention",
44
+ "full_attention",
45
+ "full_attention",
46
+ "full_attention",
47
+ "full_attention",
48
+ "full_attention",
49
+ "full_attention",
50
+ "full_attention",
51
+ "full_attention",
52
+ "full_attention",
53
+ "full_attention",
54
+ "full_attention",
55
+ "full_attention"
56
+ ],
57
+ "max_position_embeddings": 40960,
58
+ "max_window_layers": 40,
59
+ "model_type": "qwen3",
60
+ "num_attention_heads": 40,
61
+ "num_hidden_layers": 40,
62
+ "num_key_value_heads": 8,
63
+ "pad_token_id": 151643,
64
+ "rms_norm_eps": 1e-06,
65
+ "rope_parameters": {
66
+ "rope_theta": 1000000,
67
+ "rope_type": "default"
68
+ },
69
+ "sliding_window": null,
70
+ "tie_word_embeddings": false,
71
+ "transformers_version": "5.13.0.dev0",
72
+ "use_cache": false,
73
+ "use_sliding_window": false,
74
+ "vocab_size": 157184
75
+ }
dna_config.json ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "k": 6,
3
+ "dna_start_id": 151669,
4
+ "dna_vocab_size": 5515,
5
+ "dna_special_tokens": [
6
+ "<dna>",
7
+ "</dna>",
8
+ "<oov>"
9
+ ],
10
+ "auto_dna_tags": true,
11
+ "tail": "pad_n"
12
+ }
generation_config.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "do_sample": true,
3
+ "eos_token_id": [
4
+ 151645,
5
+ 151643
6
+ ],
7
+ "pad_token_id": 151643,
8
+ "temperature": 0.6,
9
+ "top_k": 20,
10
+ "top_p": 0.95,
11
+ "transformers_version": "5.13.0.dev0",
12
+ "trust_remote_code": true
13
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b44d3ffa2ef0b162f5efb61f4ae12999633201425cfb76a0f691ace04fed48c1
3
+ size 29644145312
tokenizer.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be75606093db2094d7cd20f3c2f385c212750648bd6ea4fb2bf507a6a4c55506
3
+ size 11422650
tokenizer.py ADDED
@@ -0,0 +1,835 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ HybridDNATokenizer: Combines Qwen3 BPE tokenization with DNA 6-mer tokenization.
3
+
4
+ DNA spans are detected automatically or supplied in <dna>...</dna> tags and
5
+ tokenized as 6-mers. All remaining text uses Qwen3's BPE tokenization.
6
+
7
+ Supports token_mask for Fine-grained Nucleotide Supervision (FNS):
8
+ -2: padding token
9
+ -1: text token (BPE)
10
+ 0: DNA special token (<dna>, </dna>, <oov>)
11
+ 1-5: partial 6-mer token — valid_length real bases at positions [0, valid_length),
12
+ right-padded with 'A' at positions [valid_length, k) so loss can supervise
13
+ positions 0..valid_len-1 via pos_mask = (valid_len > pos)
14
+ 6: full 6-mer
15
+ """
16
+
17
+ import os
18
+ import json
19
+ import itertools
20
+ import re
21
+ from typing import List, Optional, Tuple, Dict, Union, Any
22
+
23
+ from transformers import PreTrainedTokenizer, AutoTokenizer, BatchEncoding
24
+
25
+ # The BPE vocabulary comes from this tokenizer, and its size sets where the DNA ids start. Pin it so that a change on the
26
+ # Hub cannot silently shift the ids that checkpoints were trained with.
27
+ BASE_TOKENIZER = "Qwen/Qwen3-4B-Base"
28
+ BASE_TOKENIZER_REVISION = "906bfd4b4dc7f14ee4320094d8b41684abff8539"
29
+
30
+
31
+ class HybridDNATokenizer(PreTrainedTokenizer):
32
+ """
33
+ Hybrid tokenizer combining Qwen3 BPE with DNA 6-mer tokenization.
34
+
35
+ Explicit <dna>...</dna> regions are always tokenized as 6-mers. By default,
36
+ untagged DNA spans embedded in text are detected and tagged automatically;
37
+ pass auto_dna_tags=False to encode() or __call__() to disable detection for
38
+ one call.
39
+ """
40
+
41
+ model_input_names = ["input_ids", "attention_mask"]
42
+
43
+ # Single-nucleotide alphabet for tail="char": standard bases (A C T G U) +
44
+ # IUPAC ambiguity codes (R Y S W K M B D H V N). These 16 chars match
45
+ # microsoft/NatureLM-8x7B nlm_tokenizer.py's single-char DNA set and the
46
+ # NatureLM paper's stated DNA vocabulary size of 16 (arXiv:2502.07527, S2.1).
47
+ SINGLE_BASE_ALPHABET = "ACTGURYSWKMBDHVN"
48
+ IUPAC_DNA_ALPHABET = "ACGTRYSWKMBDHVN"
49
+ STANDARD_DNA_ALPHABET = frozenset("ACGT")
50
+ IUPAC_RUN_RE = re.compile(f"[{IUPAC_DNA_ALPHABET}]+", re.IGNORECASE | re.ASCII)
51
+ SPACED_DNA_RE = re.compile(
52
+ r"(?<![A-Za-z])(?:[ACGT]{1,5}[ \t]+){5,}[ACGT]{1,5}(?![A-Za-z])"
53
+ )
54
+
55
+ def __init__(
56
+ self,
57
+ base_tokenizer_path: Optional[str] = None,
58
+ k: int = 6,
59
+ auto_dna_tags: bool = True,
60
+ tail: str = "pad_a",
61
+ **kwargs
62
+ ):
63
+ if tail not in ("pad_a", "pad_n", "char"):
64
+ raise ValueError(f"tail must be 'pad_a', 'pad_n' or 'char', got {tail!r}")
65
+ self.tail = tail
66
+ self.pad_char = {"pad_a": "A", "pad_n": "N"}.get(tail)
67
+ self.k = k
68
+
69
+ # Load base tokenizer (Qwen3-4B-Base) at the pinned revision
70
+ self._base_tokenizer = AutoTokenizer.from_pretrained(BASE_TOKENIZER, revision=BASE_TOKENIZER_REVISION)
71
+
72
+ # Get base vocabulary
73
+ self._base_vocab = self._base_tokenizer.get_vocab()
74
+ self._base_vocab_size = len(self._base_vocab)
75
+
76
+ # Initialize DNA vocabulary
77
+ self._init_dna_vocab()
78
+
79
+ # Build combined vocabulary
80
+ self._build_combined_vocab()
81
+
82
+ # Set special tokens
83
+ self._eos_token = kwargs.pop('eos_token', None) or "<|endoftext|>"
84
+ self._pad_token = kwargs.pop('pad_token', None) or self._base_tokenizer.pad_token or "<|endoftext|>"
85
+
86
+ # Initialize parent class
87
+ super().__init__(
88
+ eos_token=self._eos_token,
89
+ pad_token=self._pad_token,
90
+ **kwargs
91
+ )
92
+
93
+ # Preserve the base tokenizer's AddedToken metadata. These tokens are
94
+ # already present in the combined vocabulary at their original IDs, so
95
+ # this only registers their metadata and does not grow the vocabulary.
96
+ self.add_tokens(list(self._base_tokenizer.added_tokens_decoder.values()))
97
+
98
+ self.special_tokens = self.dna_special_tokens + [self._eos_token, self._pad_token]
99
+ self.auto_dna_tags = auto_dna_tags
100
+
101
+ def _init_dna_vocab(self):
102
+ """Initialize DNA vocabulary (special tokens + k-mers + padding for 128 alignment)."""
103
+ bases = ['A', 'T', 'C', 'G']
104
+
105
+ # DNA special tokens
106
+ self.dna_special_tokens = ["<dna>", "</dna>", "<oov>"]
107
+
108
+ # Generate all k-mer combinations (4^k = 4096 for k=6)
109
+ self.kmers = [''.join(kmer) for kmer in itertools.product(bases, repeat=self.k)]
110
+
111
+ # tail="pad_n": dedicated tokens for the N-padded tail ("CT" -> "CTNNNN"),
112
+ # one per real prefix of length 1..k-1, i.e. 4^1+...+4^(k-1) = 1364.
113
+ self.npad_tokens = []
114
+ if self.tail == "pad_n":
115
+ for prefix_len in range(1, self.k):
116
+ for prefix in itertools.product(bases, repeat=prefix_len):
117
+ self.npad_tokens.append(''.join(prefix) + 'N' * (self.k - prefix_len))
118
+
119
+ # tail="char": single-base tokens for the trailing < k bases (NatureLM
120
+ # IUPAC alphabet), so N / ambiguity codes fall back to a token, not <oov>.
121
+ self.base_tokens = []
122
+ self.dna_base_token = {} # base -> token, e.g. "A" -> "<b_A>"
123
+ self.dna_base_token_to_base = {} # token -> base
124
+ if self.tail == "char":
125
+ for b in self.SINGLE_BASE_ALPHABET:
126
+ tok = f"<b_{b}>"
127
+ self.base_tokens.append(tok)
128
+ self.dna_base_token[b] = tok
129
+ self.dna_base_token_to_base[tok] = b
130
+
131
+ # DNA tokens start after base vocabulary
132
+ self.dna_start_id = self._base_vocab_size
133
+
134
+ # All DNA tokens get new IDs (no reuse of base vocab IDs, even for
135
+ # overlapping tokens like CCCCCC — they have different semantics in
136
+ # DNA context vs BPE context, per Qiuyi's recommendation). Tail-variant
137
+ # tokens are listed AFTER the 4^k k-mers so the k-mer ids are unchanged.
138
+ base_dna_tokens = self.dna_special_tokens + self.kmers + self.npad_tokens + self.base_tokens
139
+
140
+ # Calculate padding for 128 alignment
141
+ total_vocab_unpadded = self._base_vocab_size + len(base_dna_tokens)
142
+ target_vocab_size = ((total_vocab_unpadded + 127) // 128) * 128
143
+ num_padding_tokens = target_vocab_size - total_vocab_unpadded
144
+
145
+ # Add unused padding tokens
146
+ self.padding_tokens = [f"<unused_{i}>" for i in range(num_padding_tokens)]
147
+
148
+ # Create DNA token mappings — all get sequential new IDs
149
+ self.dna_token_to_id = {}
150
+ self.dna_id_to_token = {}
151
+
152
+ current_id = self.dna_start_id
153
+ for token in base_dna_tokens:
154
+ self.dna_token_to_id[token] = current_id
155
+ self.dna_id_to_token[current_id] = token
156
+ current_id += 1
157
+
158
+ # Add padding tokens
159
+ for token in self.padding_tokens:
160
+ self.dna_token_to_id[token] = current_id
161
+ self.dna_id_to_token[current_id] = token
162
+ current_id += 1
163
+
164
+ self.dna_vocab_size = len(base_dna_tokens) + len(self.padding_tokens)
165
+
166
+ # Set DNA special token IDs
167
+ self.dna_begin_token_id = self.dna_token_to_id["<dna>"]
168
+ self.dna_end_token_id = self.dna_token_to_id["</dna>"]
169
+ self.oov_token_id = self.dna_token_to_id["<oov>"]
170
+
171
+ def _build_combined_vocab(self):
172
+ """Build combined vocabulary (base + DNA)."""
173
+ self._vocab = self._base_vocab.copy()
174
+
175
+ for token, token_id in self.dna_token_to_id.items():
176
+ if token not in self._vocab:
177
+ self._vocab[token] = token_id
178
+
179
+ self._id_to_token = {v: k for k, v in self._vocab.items()}
180
+ for token_id, token in self.dna_id_to_token.items():
181
+ if token_id not in self._id_to_token:
182
+ self._id_to_token[token_id] = token
183
+
184
+ @property
185
+ def vocab_size(self) -> int:
186
+ return max(self._vocab.values()) + 1
187
+
188
+ def get_vocab(self) -> Dict[str, int]:
189
+ return self._vocab.copy()
190
+
191
+ def __len__(self):
192
+ # Override default (len(get_vocab())) because get_vocab() deduplicates
193
+ # CCCCCC which exists as both BPE (ID 91443) and DNA 6-mer (ID 154402).
194
+ return self.vocab_size
195
+
196
+ @classmethod
197
+ def _auto_tag_text_segment(cls, text: str) -> str:
198
+ """Tag conservative DNA candidates in a text-only segment.
199
+
200
+ Each maximal IUPAC nucleotide run is considered independently. Pure
201
+ A/C/G/T runs need at least six bases. Runs containing IUPAC ambiguity
202
+ codes need at least twelve bases and may contain at most 20% ambiguity.
203
+ Rejected ambiguous runs stay wholly on the BPE path rather than having
204
+ any A/C/G/T substring tagged.
205
+ """
206
+ parts = []
207
+ previous_end = 0
208
+
209
+ for match in cls.IUPAC_RUN_RE.finditer(text):
210
+ candidate = match.group(0)
211
+ normalized = candidate.upper()
212
+ ambiguous_count = sum(base not in cls.STANDARD_DNA_ALPHABET for base in normalized)
213
+ is_standard_dna = ambiguous_count == 0 and len(normalized) >= 6
214
+ is_conservative_iupac = (
215
+ ambiguous_count > 0
216
+ and len(normalized) >= 12
217
+ and ambiguous_count / len(normalized) <= 0.2
218
+ )
219
+
220
+ if not (is_standard_dna or is_conservative_iupac):
221
+ continue
222
+
223
+ parts.append(text[previous_end:match.start()])
224
+ parts.append(f"<dna>{normalized}</dna>")
225
+ previous_end = match.end()
226
+
227
+ if not parts:
228
+ return text
229
+
230
+ parts.append(text[previous_end:])
231
+ return "".join(parts)
232
+
233
+ @classmethod
234
+ def _normalize_spaced_dna(cls, text: str) -> str:
235
+ """Join uppercase DNA displays with at least six chunks and twelve bases.
236
+
237
+ Chunks contain one to five A/C/G/T bases, separated only by spaces or
238
+ tabs. Alphabetic boundaries prevent joining fragments of words.
239
+ """
240
+ def normalize(match):
241
+ sequence = "".join(match.group(0).split())
242
+ return sequence if len(sequence) >= 12 else match.group(0)
243
+
244
+ return cls.SPACED_DNA_RE.sub(normalize, text)
245
+
246
+ def _add_auto_dna_tags(self, text: str) -> str:
247
+ """Tag DNA outside existing <dna>...</dna> regions."""
248
+ return "".join(
249
+ segment if is_dna else self._auto_tag_text_segment(self._normalize_spaced_dna(segment))
250
+ for segment, is_dna in self._split_by_dna_tags(text)
251
+ )
252
+
253
+ def _split_by_dna_tags(self, text: str) -> List[Tuple[str, bool]]:
254
+ segments = []
255
+ i = 0
256
+ n = len(text)
257
+
258
+ while i < n:
259
+ start_pos = text.find('<dna>', i)
260
+ end_pos = text.find('</dna>', i)
261
+
262
+ if start_pos == -1 and end_pos == -1:
263
+ remaining = text[i:]
264
+ if remaining:
265
+ segments.append((remaining, False))
266
+ break
267
+
268
+ if start_pos == -1 and end_pos != -1:
269
+ dna_region = text[i:end_pos + 6]
270
+ if dna_region:
271
+ segments.append((dna_region, True))
272
+ i = end_pos + 6
273
+ continue
274
+
275
+ if start_pos != -1 and end_pos == -1:
276
+ if i < start_pos:
277
+ normal_text = text[i:start_pos]
278
+ if normal_text:
279
+ segments.append((normal_text, False))
280
+ dna_region = text[start_pos:]
281
+ if dna_region:
282
+ segments.append((dna_region, True))
283
+ break
284
+
285
+ if start_pos < end_pos:
286
+ if i < start_pos:
287
+ normal_text = text[i:start_pos]
288
+ if normal_text:
289
+ segments.append((normal_text, False))
290
+ dna_region = text[start_pos:end_pos + 6]
291
+ if dna_region:
292
+ segments.append((dna_region, True))
293
+ i = end_pos + 6
294
+ else:
295
+ dna_region = text[i:end_pos + 6]
296
+ if dna_region:
297
+ segments.append((dna_region, True))
298
+ i = end_pos + 6
299
+
300
+ return segments
301
+
302
+ def _parse_dna_region(self, dna_region: str) -> Tuple[str, bool, bool]:
303
+ if dna_region == '<dna>':
304
+ return '', True, False
305
+ elif dna_region == '</dna>':
306
+ return '', False, True
307
+
308
+ has_start = dna_region.startswith('<dna>')
309
+ has_end = dna_region.endswith('</dna>')
310
+
311
+ content = dna_region
312
+ if has_start:
313
+ content = content[5:]
314
+ if has_end and content.endswith('</dna>'):
315
+ content = content[:-6]
316
+
317
+ return content.strip(), has_start, has_end
318
+
319
+ def _process_dna_sequence(self, dna_seq: str) -> Dict:
320
+ k = self.k
321
+ dna_seq = dna_seq.upper()
322
+
323
+ kmer_tokens = []
324
+ valid_bases = set('ATCG')
325
+
326
+ def is_valid_kmer(kmer):
327
+ return len(kmer) == k and all(base in valid_bases for base in kmer)
328
+
329
+ if self.tail == "char":
330
+ # Greedy NatureLM tokenization (NatureLM, arXiv:2502.07527), ported
331
+ # from microsoft/NatureLM-8x7B nlm_tokenizer.py::_tokenize_entity:
332
+ # take a full k-mer when the next k chars form one, else emit a single
333
+ # base token and advance by 1 (also re-aligns after any non-base char).
334
+ i = 0
335
+ n = len(dna_seq)
336
+ while i < n:
337
+ window = dna_seq[i:i + k]
338
+ if is_valid_kmer(window):
339
+ kmer_tokens.append(window)
340
+ i += k
341
+ else:
342
+ base = dna_seq[i]
343
+ kmer_tokens.append(self.dna_base_token.get(base, "<oov>"))
344
+ i += 1
345
+ return {"kmer_tokens": kmer_tokens, "padding_length": 0, "valid_length": k}
346
+
347
+ for i in range(0, len(dna_seq) - k + 1, k):
348
+ kmer = dna_seq[i:i+k]
349
+ if is_valid_kmer(kmer):
350
+ kmer_tokens.append(kmer)
351
+ else:
352
+ kmer_tokens.append("<oov>")
353
+
354
+ processed_length = len(kmer_tokens) * k
355
+ remaining = dna_seq[processed_length:]
356
+ padding_length = 0
357
+ valid_length = k
358
+
359
+ if remaining:
360
+ padding_needed = k - len(remaining)
361
+ # Right-pad up to a full k-mer: real bases occupy positions
362
+ # [0, valid_length). The hybrid BP loss supervises positions
363
+ # 0..valid_len-1 via pos_mask = (valid_len > pos), so padding must be
364
+ # at the END. pad_char="A" reuses a real k-mer id ("CT"->"CTAAAA");
365
+ # "N" maps to a dedicated tail token ("CT"->"CTNNNN").
366
+ padded = remaining + self.pad_char * padding_needed
367
+
368
+ if all(base in valid_bases for base in remaining):
369
+ kmer_tokens.append(padded)
370
+ else:
371
+ kmer_tokens.append("<oov>")
372
+
373
+ padding_length = padding_needed
374
+ valid_length = len(remaining)
375
+
376
+ return {
377
+ "kmer_tokens": kmer_tokens,
378
+ "padding_length": padding_length,
379
+ "valid_length": valid_length,
380
+ }
381
+
382
+ def _tokenize(self, text: str, **kwargs) -> List[str]:
383
+ return list(text)
384
+
385
+ def _convert_token_to_id(self, token: str) -> int:
386
+ if token in self.dna_token_to_id:
387
+ return self.dna_token_to_id[token]
388
+ return self._base_vocab.get(token, self._base_tokenizer.unk_token_id or 0)
389
+
390
+ def _convert_id_to_token(self, index: int) -> str:
391
+ if index in self.dna_id_to_token:
392
+ return self.dna_id_to_token[index]
393
+ return self._id_to_token.get(index, "<oov>")
394
+
395
+ def convert_tokens_to_string(self, tokens: List[str]) -> str:
396
+ ids = [self._convert_token_to_id(t) for t in tokens]
397
+ return self.decode(ids, skip_special_tokens=False)
398
+ def encode(
399
+ self,
400
+ text: str,
401
+ add_special_tokens: bool = False,
402
+ return_token_mask: bool = False,
403
+ auto_dna_tags: Optional[bool] = None,
404
+ **kwargs
405
+ ) -> Union[List[int], Tuple[List[int], List[int]]]:
406
+ use_auto = self.auto_dna_tags if auto_dna_tags is None else auto_dna_tags
407
+ if use_auto:
408
+ text = self._add_auto_dna_tags(text)
409
+
410
+ segments = self._split_by_dna_tags(text)
411
+
412
+ token_ids = []
413
+ token_mask = [] if return_token_mask else None
414
+
415
+ for segment_content, is_dna in segments:
416
+ if is_dna:
417
+ dna_content, has_start, has_end = self._parse_dna_region(segment_content)
418
+
419
+ if has_start:
420
+ token_ids.append(self.dna_begin_token_id)
421
+ if return_token_mask:
422
+ token_mask.append(0)
423
+
424
+ if dna_content:
425
+ result = self._process_dna_sequence(dna_content)
426
+
427
+ for idx, kmer in enumerate(result["kmer_tokens"]):
428
+ token_id = self.dna_token_to_id.get(kmer, self.oov_token_id)
429
+ token_ids.append(token_id)
430
+
431
+ if return_token_mask:
432
+ if kmer == "<oov>":
433
+ token_mask.append(0)
434
+ elif kmer in self.dna_base_token_to_base:
435
+ token_mask.append(1) # char-fallback single base
436
+ elif idx == len(result["kmer_tokens"]) - 1 and result["padding_length"] > 0:
437
+ token_mask.append(result["valid_length"])
438
+ else:
439
+ token_mask.append(self.k)
440
+
441
+ if has_end:
442
+ token_ids.append(self.dna_end_token_id)
443
+ if return_token_mask:
444
+ token_mask.append(0)
445
+ else:
446
+ base_ids = self._base_tokenizer.encode(
447
+ segment_content,
448
+ add_special_tokens=False
449
+ )
450
+ token_ids.extend(base_ids)
451
+ if return_token_mask:
452
+ token_mask.extend([-1] * len(base_ids))
453
+
454
+ # Do NOT append EOS when add_special_tokens=True. Qwen3 doesn't add
455
+ # BOS/EOS either, and appending EOS here breaks lighteval's
456
+ # tok_encode_pair: it relies on
457
+ # len(encode(ctx)) + len(encode(answer)) == len(encode(ctx + answer))
458
+ # which the extra EOS violates by shifting the split by 1.
459
+
460
+ if return_token_mask:
461
+ return token_ids, token_mask
462
+ return token_ids
463
+
464
+ def _decode_dna_token(self, token_str: str) -> str:
465
+ # Surface text for a DNA-vocab token. Baseline (pad_a) returns it
466
+ # unchanged; char maps "<b_A>"->"A"; pad_n strips the N-pad tail fill
467
+ # ("CTNNNN"->"CT"). Real k-mers contain no N, so rstrip is a no-op there.
468
+ if token_str in self.dna_base_token_to_base:
469
+ return self.dna_base_token_to_base[token_str]
470
+ if self.tail == "pad_n" and len(token_str) == self.k and token_str[0] in "ATCG":
471
+ return token_str.rstrip("N")
472
+ return token_str
473
+
474
+ def decode(
475
+ self,
476
+ token_ids: Union[int, List[int]],
477
+ skip_special_tokens: bool = False,
478
+ **kwargs
479
+ ) -> str:
480
+ # A raw tensor/ndarray must become plain ints: dna_id_to_token is keyed by
481
+ # python ints and a 0-dim tensor scalar hashes by identity, so DNA lookups
482
+ # silently miss and the bases are dropped (e.g. decode(generate(...)[0])).
483
+ if hasattr(token_ids, "tolist"):
484
+ token_ids = token_ids.tolist()
485
+ if isinstance(token_ids, int):
486
+ token_ids = [token_ids]
487
+
488
+ if skip_special_tokens:
489
+ special_ids = {self.eos_token_id, self.pad_token_id}
490
+ token_ids = [tid for tid in token_ids if tid not in special_ids]
491
+
492
+ parts = []
493
+ i = 0
494
+
495
+ while i < len(token_ids):
496
+ tid = token_ids[i]
497
+
498
+ if tid == self.dna_begin_token_id:
499
+ dna_tokens = []
500
+ i += 1
501
+
502
+ while i < len(token_ids) and token_ids[i] != self.dna_end_token_id:
503
+ if token_ids[i] in self.dna_id_to_token:
504
+ dna_tokens.append(self._decode_dna_token(self.dna_id_to_token[token_ids[i]]))
505
+ i += 1
506
+
507
+ dna_seq = ''.join(dna_tokens)
508
+
509
+ if skip_special_tokens:
510
+ parts.append(dna_seq)
511
+ else:
512
+ parts.append(f"<dna>{dna_seq}")
513
+ if i < len(token_ids) and token_ids[i] == self.dna_end_token_id:
514
+ parts.append("</dna>")
515
+ i += 1
516
+
517
+ elif tid in self.dna_id_to_token:
518
+ # This branch handles k-mer tokens that appear without a <dna>
519
+ # wrapper — the common generation case where <dna> was in the
520
+ # prompt but only the generated portion is being decoded.
521
+ # K-mer tokens are content, not special tokens, so always decode
522
+ # them. Only drop true DNA special tokens (<dna>, </dna>, <oov>)
523
+ # when skip_special_tokens=True.
524
+ is_dna_special = tid in (self.dna_begin_token_id, self.dna_end_token_id, self.oov_token_id)
525
+ if not (skip_special_tokens and is_dna_special):
526
+ parts.append(self._decode_dna_token(self.dna_id_to_token[tid]))
527
+ i += 1
528
+
529
+ else:
530
+ text_ids = []
531
+ while i < len(token_ids):
532
+ curr_id = token_ids[i]
533
+ if curr_id in self.dna_id_to_token or curr_id == self.dna_begin_token_id:
534
+ break
535
+ text_ids.append(curr_id)
536
+ i += 1
537
+
538
+ if text_ids:
539
+ decoded = self._base_tokenizer.decode(text_ids, skip_special_tokens=skip_special_tokens)
540
+ parts.append(decoded)
541
+
542
+ return ''.join(parts)
543
+
544
+ def batch_decode(
545
+ self,
546
+ sequences: Union[List[int], List[List[int]], "torch.Tensor"],
547
+ skip_special_tokens: bool = False,
548
+ **kwargs
549
+ ) -> List[str]:
550
+ return [
551
+ self.decode(
552
+ seq.tolist() if hasattr(seq, 'tolist') else list(seq),
553
+ skip_special_tokens=skip_special_tokens,
554
+ **kwargs
555
+ )
556
+ for seq in sequences
557
+ ]
558
+
559
+ def __call__(
560
+ self,
561
+ text: Union[str, List[str]],
562
+ add_special_tokens: bool = False,
563
+ padding: bool = False,
564
+ truncation: bool = False,
565
+ max_length: Optional[int] = None,
566
+ return_tensors: Optional[str] = None,
567
+ return_token_mask: bool = False,
568
+ auto_dna_tags: Optional[bool] = None,
569
+ **kwargs
570
+ ) -> Dict[str, Any]:
571
+ is_batch = isinstance(text, list)
572
+ texts = text if is_batch else [text]
573
+
574
+ all_ids = []
575
+ all_masks = [] if return_token_mask else None
576
+
577
+ for t in texts:
578
+ if return_token_mask:
579
+ ids, mask = self.encode(t, add_special_tokens=add_special_tokens, return_token_mask=True, auto_dna_tags=auto_dna_tags)
580
+ all_ids.append(ids)
581
+ all_masks.append(mask)
582
+ else:
583
+ ids = self.encode(t, add_special_tokens=add_special_tokens, return_token_mask=False, auto_dna_tags=auto_dna_tags)
584
+ all_ids.append(ids)
585
+
586
+ if padding:
587
+ max_len = max(len(ids) for ids in all_ids)
588
+ if max_length:
589
+ max_len = min(max_len, max_length)
590
+
591
+ padded_ids = []
592
+ attention_masks = []
593
+ padded_token_masks = [] if return_token_mask else None
594
+
595
+ for idx, ids in enumerate(all_ids):
596
+ pad_len = max_len - len(ids)
597
+
598
+ if pad_len > 0:
599
+ ids = ids + [self.pad_token_id] * pad_len
600
+ attn = [1] * (max_len - pad_len) + [0] * pad_len
601
+ if return_token_mask:
602
+ mask = all_masks[idx] + [-2] * pad_len
603
+ else:
604
+ ids = ids[:max_len]
605
+ attn = [1] * max_len
606
+ if return_token_mask:
607
+ mask = all_masks[idx][:max_len]
608
+
609
+ padded_ids.append(ids)
610
+ attention_masks.append(attn)
611
+ if return_token_mask:
612
+ padded_token_masks.append(mask)
613
+
614
+ all_ids = padded_ids
615
+ all_masks = padded_token_masks
616
+ else:
617
+ attention_masks = [[1] * len(ids) for ids in all_ids]
618
+
619
+ result = {
620
+ "input_ids": all_ids if is_batch else all_ids[0],
621
+ "attention_mask": attention_masks if is_batch else attention_masks[0],
622
+ }
623
+
624
+ if return_token_mask:
625
+ result["token_mask"] = all_masks if is_batch else all_masks[0]
626
+
627
+ if return_tensors == "pt":
628
+ import torch
629
+ if is_batch:
630
+ result["input_ids"] = torch.tensor(result["input_ids"])
631
+ result["attention_mask"] = torch.tensor(result["attention_mask"])
632
+ if return_token_mask:
633
+ result["token_mask"] = torch.tensor(result["token_mask"])
634
+ else:
635
+ result["input_ids"] = torch.tensor([result["input_ids"]])
636
+ result["attention_mask"] = torch.tensor([result["attention_mask"]])
637
+ if return_token_mask:
638
+ result["token_mask"] = torch.tensor([result["token_mask"]])
639
+
640
+ return BatchEncoding(result, tensor_type=return_tensors)
641
+
642
+ # Markers delimiting an assistant turn, used to build the assistant-token mask.
643
+ # Qwen chat format; override on the instance for other bases (e.g. Gemma uses
644
+ # "<start_of_turn>model\n" / "<end_of_turn>").
645
+ ASSISTANT_HEADER = "<|im_start|>assistant\n"
646
+ TURN_END = "<|im_end|>\n"
647
+
648
+ def apply_chat_template(self, conversation, *args, return_assistant_tokens_mask: bool = False, **kwargs):
649
+ """Completion-only assistant mask WITHOUT a fast tokenizer.
650
+
651
+ HF derives the assistant mask from ``{% generation %}`` char-spans via
652
+ ``BatchEncoding.char_to_token()``, which only fast (Rust) tokenizers implement.
653
+ HybridDNATokenizer is a slow Python tokenizer (the DNA k-mer logic can't be a
654
+ ``tokenizer.json``), so that path raises
655
+ ``char_to_token() is not available when using Python based tokenizers``.
656
+
657
+ Instead we tokenize normally and mark the tokens of every assistant turn -- from
658
+ just after ``ASSISTANT_HEADER`` up to and including the next ``TURN_END``. This
659
+ makes ``return_assistant_tokens_mask=True`` (and thus TRL ``assistant_only_loss``)
660
+ work unchanged, and needs no ``{% generation %}`` markers in the template.
661
+ """
662
+ auto_dna_tags = kwargs.pop("auto_dna_tags", None)
663
+ if auto_dna_tags is not None:
664
+ tokenizer_kwargs = dict(kwargs.pop("tokenizer_kwargs", None) or {})
665
+ tokenizer_kwargs["auto_dna_tags"] = auto_dna_tags
666
+ kwargs["tokenizer_kwargs"] = tokenizer_kwargs
667
+
668
+ if not return_assistant_tokens_mask:
669
+ return super().apply_chat_template(conversation, *args, **kwargs)
670
+ kwargs.pop("return_assistant_tokens_mask", None)
671
+ want_tensors = kwargs.pop("return_tensors", None)
672
+ kwargs["tokenize"] = True
673
+ kwargs["return_dict"] = True
674
+ enc = super().apply_chat_template(conversation, *args, return_tensors=None, **kwargs)
675
+ seqs = enc["input_ids"]
676
+ batched = len(seqs) > 0 and isinstance(seqs[0], (list, tuple))
677
+ batch = seqs if batched else [seqs]
678
+ hdr = self(self.ASSISTANT_HEADER, add_special_tokens=False)["input_ids"]
679
+ end = self(self.TURN_END, add_special_tokens=False)["input_ids"]
680
+ masks = []
681
+ for ids in batch:
682
+ ids = list(ids)
683
+ m = [0] * len(ids)
684
+ i = 0
685
+ while i <= len(ids) - len(hdr):
686
+ if ids[i:i + len(hdr)] == hdr:
687
+ j = i + len(hdr)
688
+ while j < len(ids) and ids[j:j + len(end)] != end:
689
+ m[j] = 1
690
+ j += 1
691
+ for k in range(len(end)): # include the turn-end token(s)
692
+ if j + k < len(ids):
693
+ m[j + k] = 1
694
+ i = j + len(end)
695
+ else:
696
+ i += 1
697
+ masks.append(m)
698
+ enc["assistant_masks"] = masks if batched else masks[0]
699
+ if want_tensors is not None:
700
+ enc = enc.convert_to_tensors(tensor_type=want_tensors)
701
+ return enc
702
+
703
+ def save_vocabulary(self, save_directory: str, filename_prefix: Optional[str] = None) -> Tuple[str]:
704
+ vocab_file = os.path.join(
705
+ save_directory,
706
+ (filename_prefix + "-" if filename_prefix else "") + "vocab.json"
707
+ )
708
+
709
+ with open(vocab_file, "w", encoding="utf-8") as f:
710
+ json.dump(self._vocab, f, ensure_ascii=False, indent=2)
711
+
712
+ return (vocab_file,)
713
+
714
+ def save_pretrained(self, save_directory: str, **kwargs):
715
+ os.makedirs(save_directory, exist_ok=True)
716
+
717
+ # Save base tokenizer files with the wrapper's effective chat template.
718
+ self._base_tokenizer.chat_template = self.chat_template
719
+ self._base_tokenizer.save_pretrained(save_directory)
720
+
721
+ # Save DNA config
722
+ dna_config = {
723
+ "k": self.k,
724
+ "dna_start_id": self.dna_start_id,
725
+ "dna_vocab_size": self.dna_vocab_size,
726
+ "dna_special_tokens": self.dna_special_tokens,
727
+ "auto_dna_tags": self.auto_dna_tags,
728
+ "tail": self.tail,
729
+ }
730
+
731
+ dna_config_path = os.path.join(save_directory, "dna_config.json")
732
+ with open(dna_config_path, "w", encoding="utf-8") as f:
733
+ json.dump(dna_config, f, indent=2)
734
+
735
+ # Update tokenizer_config.json with auto_map
736
+ config_path = os.path.join(save_directory, "tokenizer_config.json")
737
+
738
+ if os.path.exists(config_path):
739
+ with open(config_path, "r") as f:
740
+ config = json.load(f)
741
+ else:
742
+ config = {}
743
+
744
+ config.update({
745
+ "tokenizer_class": "HybridDNATokenizer",
746
+ "auto_map": {
747
+ "AutoTokenizer": ["tokenizer.HybridDNATokenizer", None]
748
+ },
749
+ "k": self.k,
750
+ "auto_dna_tags": self.auto_dna_tags,
751
+ "tail": self.tail,
752
+ })
753
+
754
+ with open(config_path, "w", encoding="utf-8") as f:
755
+ json.dump(config, f, indent=2, ensure_ascii=False)
756
+
757
+ # Copy this tokenizer.py to save directory
758
+ import shutil
759
+ src_py = os.path.abspath(__file__)
760
+ dst_py = os.path.join(save_directory, "tokenizer.py")
761
+ if os.path.exists(src_py) and src_py != dst_py:
762
+ shutil.copy2(src_py, dst_py)
763
+
764
+ return (save_directory,)
765
+
766
+ @classmethod
767
+ def from_pretrained(
768
+ cls,
769
+ pretrained_model_name_or_path,
770
+ *args,
771
+ cache_dir=None,
772
+ force_download=False,
773
+ local_files_only=False,
774
+ token=None,
775
+ revision="main",
776
+ trust_remote_code=False,
777
+ **kwargs,
778
+ ):
779
+ # Custom loader: PreTrainedTokenizer.from_pretrained is incompatible with this
780
+ # tokenizer's __init__ (it re-passes bos_token etc.), so construct directly from
781
+ # bio_config/dna_config. That bypasses standard metadata loading, so also read
782
+ # chat_template from tokenizer_config.json (vLLM / apply_chat_template need it).
783
+ from transformers.utils.hub import cached_file
784
+
785
+ proxies = kwargs.pop("proxies", None)
786
+ subfolder = kwargs.pop("subfolder", None)
787
+ commit_hash = kwargs.pop("_commit_hash", None)
788
+ hub_kwargs = {
789
+ "cache_dir": cache_dir,
790
+ "force_download": force_download,
791
+ "local_files_only": local_files_only,
792
+ "proxies": proxies,
793
+ "revision": revision,
794
+ "subfolder": subfolder,
795
+ "token": token,
796
+ "_commit_hash": commit_hash,
797
+ "_raise_exceptions_for_missing_entries": False,
798
+ }
799
+
800
+ cfg = {}
801
+ for _name in ("bio_config.json", "dna_config.json"):
802
+ _p = cached_file(pretrained_model_name_or_path, _name, **hub_kwargs)
803
+ if _p is not None:
804
+ with open(_p, encoding="utf-8") as _f:
805
+ cfg = json.load(_f)
806
+ break
807
+
808
+ _init = {"base_tokenizer_path": pretrained_model_name_or_path}
809
+ for _key in ("k", "tail", "auto_dna_tags"):
810
+ if _key in cfg:
811
+ _init[_key] = cfg[_key]
812
+
813
+ tokenizer_config_path = cached_file(pretrained_model_name_or_path, "tokenizer_config.json", **hub_kwargs)
814
+ if tokenizer_config_path is not None:
815
+ with open(tokenizer_config_path, encoding="utf-8") as _f:
816
+ tokenizer_config = json.load(_f)
817
+ if tokenizer_config.get("chat_template"):
818
+ _init["chat_template"] = tokenizer_config["chat_template"]
819
+
820
+ chat_template_path = cached_file(pretrained_model_name_or_path, "chat_template.jinja", **hub_kwargs)
821
+ if chat_template_path is not None:
822
+ with open(chat_template_path, encoding="utf-8") as _f:
823
+ _init["chat_template"] = _f.read()
824
+
825
+ _init.update(kwargs)
826
+ tokenizer = cls(**_init)
827
+ # DNA ids start right after the base vocabulary, so a layout that differs from the saved one would misalign every
828
+ # DNA embedding. Fail instead of tokenizing with shifted ids.
829
+ for _key in ("dna_start_id", "dna_vocab_size"):
830
+ if _key in cfg and cfg[_key] != getattr(tokenizer, _key):
831
+ raise ValueError(
832
+ f"{pretrained_model_name_or_path} was saved with {_key}={cfg[_key]}, but the base tokenizer gives "
833
+ f"{getattr(tokenizer, _key)}, so its DNA token ids would not match the model."
834
+ )
835
+ return tokenizer
tokenizer_config.json ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "backend": "tokenizers",
4
+ "bos_token": null,
5
+ "clean_up_tokenization_spaces": false,
6
+ "eos_token": "<|endoftext|>",
7
+ "errors": "replace",
8
+ "extra_special_tokens": [
9
+ "<|im_start|>",
10
+ "<|im_end|>",
11
+ "<|object_ref_start|>",
12
+ "<|object_ref_end|>",
13
+ "<|box_start|>",
14
+ "<|box_end|>",
15
+ "<|quad_start|>",
16
+ "<|quad_end|>",
17
+ "<|vision_start|>",
18
+ "<|vision_end|>",
19
+ "<|vision_pad|>",
20
+ "<|image_pad|>",
21
+ "<|video_pad|>"
22
+ ],
23
+ "is_local": false,
24
+ "local_files_only": false,
25
+ "model_max_length": 131072,
26
+ "pad_token": "<|endoftext|>",
27
+ "split_special_tokens": false,
28
+ "tokenizer_class": "HybridDNATokenizer",
29
+ "unk_token": null,
30
+ "auto_map": {
31
+ "AutoTokenizer": [
32
+ "tokenizer.HybridDNATokenizer",
33
+ null
34
+ ]
35
+ },
36
+ "k": 6,
37
+ "auto_dna_tags": true,
38
+ "tail": "pad_n"
39
+ }