Gertlek commited on
Commit
cfacbcc
·
verified ·
1 Parent(s): f5d85f5

Special token bug fix

Browse files
Files changed (2) hide show
  1. llada_guard/data.py +86 -21
  2. pyproject.toml +1 -1
llada_guard/data.py CHANGED
@@ -45,13 +45,62 @@ class SafetyDataset(Dataset):
45
  def __len__(self):
46
  return len(self.examples)
47
 
48
- def _chat_token_count(self, content: str) -> int:
 
 
49
  prompt_str = self.tokenizer.apply_chat_template(
50
  [{"role": "user", "content": content}],
51
  add_generation_prompt=True,
52
  tokenize=False,
53
  )
54
- return len(self.tokenizer(prompt_str)["input_ids"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
 
56
  def _gen_total_len(
57
  self,
@@ -60,15 +109,19 @@ class SafetyDataset(Dataset):
60
  agent_response: Optional[str],
61
  categories: Optional[list[str]],
62
  violated_categories: Optional[list[str]] = None,
 
63
  ) -> int:
64
- prompt_len = self._chat_token_count(
65
- build_prompt(
66
- user_message,
67
- agent_response,
68
- categories=categories,
69
- prompt_format=self.prompt_format,
70
- )
71
  )
 
 
 
 
 
72
  if verdict_text == "unsafe" and violated_categories:
73
  label_text = build_target(
74
  False,
@@ -91,13 +144,20 @@ class SafetyDataset(Dataset):
91
  ) -> tuple[str, Optional[str]]:
92
  """Trim the response first, then the user text, until the pair fits."""
93
  while True:
94
- gen_safe_total = self._gen_total_len("safe", user_message, agent_response, categories)
 
 
 
 
 
 
95
  gen_unsafe_total = self._gen_total_len(
96
  "unsafe",
97
  user_message,
98
  agent_response,
99
  categories,
100
  violated_categories=violated_categories,
 
101
  )
102
  overflow = max(gen_safe_total, gen_unsafe_total) - self.max_seq_length
103
  if overflow <= 0:
@@ -124,6 +184,7 @@ class SafetyDataset(Dataset):
124
  agent_response: Optional[str],
125
  categories: Optional[list[str]],
126
  violated_categories: Optional[list[str]] = None,
 
127
  ) -> tuple[torch.Tensor, torch.Tensor]:
128
  prefix, conversation_text, suffix = build_prompt_parts(
129
  user_message,
@@ -137,17 +198,10 @@ class SafetyDataset(Dataset):
137
  add_generation_prompt=True,
138
  tokenize=False,
139
  )
140
- prompt_encoding = self.tokenizer(
141
- prompt_str,
142
- add_special_tokens=False,
143
- return_offsets_mapping=True,
144
- )
145
- prompt_ids = prompt_encoding["input_ids"]
146
- prompt_offsets = prompt_encoding["offset_mapping"]
147
-
148
- conversation_start = prompt_str.find(conversation_text)
149
- if conversation_start < 0:
150
- raise ValueError("Conversation span not found inside classification prompt")
151
 
152
  user_span = f"User: {user_message}"
153
  user_start = conversation_start
@@ -160,6 +214,14 @@ class SafetyDataset(Dataset):
160
  response_start = user_end
161
  response_end = response_start + len(response_span)
162
 
 
 
 
 
 
 
 
 
163
  prompt_block_ids = []
164
  for start, end in prompt_offsets:
165
  in_user = end > user_start and start < user_end
@@ -252,12 +314,14 @@ class SafetyDataset(Dataset):
252
  user_message,
253
  rendered_agent_response,
254
  None,
 
255
  )
256
  gen_unsafe_input_ids, gen_unsafe_block_ids = self._build_gen_sequence(
257
  "unsafe",
258
  user_message,
259
  rendered_agent_response,
260
  None,
 
261
  )
262
  hazard_label_mask = int((not is_safe) and bool(violated))
263
  if hazard_label_mask:
@@ -267,6 +331,7 @@ class SafetyDataset(Dataset):
267
  rendered_agent_response,
268
  None,
269
  violated_categories=violated,
 
270
  )
271
  else:
272
  hazard_input_ids = gen_unsafe_input_ids
 
45
  def __len__(self):
46
  return len(self.examples)
47
 
48
+ def _chat_token_count(
49
+ self, content: str, trusted_span: Optional[tuple[int, int]] = None
50
+ ) -> int:
51
  prompt_str = self.tokenizer.apply_chat_template(
52
  [{"role": "user", "content": content}],
53
  add_generation_prompt=True,
54
  tokenize=False,
55
  )
56
+ ids, _ = self._encode_chat(prompt_str, content, trusted_span)
57
+ return len(ids) + len(self.tokenizer("")["input_ids"])
58
+
59
+ def _encode_chat(
60
+ self,
61
+ prompt_str: str,
62
+ content: str,
63
+ trusted_span: Optional[tuple[int, int]] = None,
64
+ ) -> tuple[list[int], list[tuple[int, int]]]:
65
+ """Tokenize a chat-formatted prompt without parsing special tokens in content.
66
+
67
+ Special-token strings typed by a user or agent (e.g. ``<|eot_id|>``,
68
+ ``<|mdm_mask|>``) stay plain text; only the chat template's own control
69
+ tokens are parsed. ``trusted_span`` (offsets within ``content``) marks
70
+ release-generated text, such as the synthetic mask response, whose
71
+ control tokens are kept.
72
+ """
73
+ content_start = prompt_str.find(content)
74
+ if content_start < 0:
75
+ raise ValueError("Classification prompt not found inside chat template")
76
+ content_end = content_start + len(content)
77
+ segments = [(0, content_start, False)]
78
+ if trusted_span is None:
79
+ segments.append((content_start, content_end, True))
80
+ else:
81
+ trusted_start = content_start + trusted_span[0]
82
+ trusted_end = content_start + trusted_span[1]
83
+ segments += [
84
+ (content_start, trusted_start, True),
85
+ (trusted_start, trusted_end, False),
86
+ (trusted_end, content_end, True),
87
+ ]
88
+ segments.append((content_end, len(prompt_str), False))
89
+
90
+ input_ids: list[int] = []
91
+ offsets: list[tuple[int, int]] = []
92
+ for start, end, split_special in segments:
93
+ if start == end:
94
+ continue
95
+ encoding = self.tokenizer(
96
+ prompt_str[start:end],
97
+ add_special_tokens=False,
98
+ return_offsets_mapping=True,
99
+ split_special_tokens=split_special,
100
+ )
101
+ input_ids.extend(encoding["input_ids"])
102
+ offsets.extend((s + start, e + start) for s, e in encoding["offset_mapping"])
103
+ return input_ids, offsets
104
 
105
  def _gen_total_len(
106
  self,
 
109
  agent_response: Optional[str],
110
  categories: Optional[list[str]],
111
  violated_categories: Optional[list[str]] = None,
112
+ synthetic_response: bool = False,
113
  ) -> int:
114
+ prefix, conversation, suffix = build_prompt_parts(
115
+ user_message,
116
+ agent_response,
117
+ categories=categories,
118
+ prompt_format=self.prompt_format,
 
 
119
  )
120
+ trusted_span = None
121
+ if synthetic_response and agent_response is not None:
122
+ trusted_end = len(prefix) + len(conversation)
123
+ trusted_span = (trusted_end - len(agent_response), trusted_end)
124
+ prompt_len = self._chat_token_count(prefix + conversation + suffix, trusted_span)
125
  if verdict_text == "unsafe" and violated_categories:
126
  label_text = build_target(
127
  False,
 
144
  ) -> tuple[str, Optional[str]]:
145
  """Trim the response first, then the user text, until the pair fits."""
146
  while True:
147
+ gen_safe_total = self._gen_total_len(
148
+ "safe",
149
+ user_message,
150
+ agent_response,
151
+ categories,
152
+ synthetic_response=not allow_agent_drop,
153
+ )
154
  gen_unsafe_total = self._gen_total_len(
155
  "unsafe",
156
  user_message,
157
  agent_response,
158
  categories,
159
  violated_categories=violated_categories,
160
+ synthetic_response=not allow_agent_drop,
161
  )
162
  overflow = max(gen_safe_total, gen_unsafe_total) - self.max_seq_length
163
  if overflow <= 0:
 
184
  agent_response: Optional[str],
185
  categories: Optional[list[str]],
186
  violated_categories: Optional[list[str]] = None,
187
+ synthetic_response: bool = False,
188
  ) -> tuple[torch.Tensor, torch.Tensor]:
189
  prefix, conversation_text, suffix = build_prompt_parts(
190
  user_message,
 
198
  add_generation_prompt=True,
199
  tokenize=False,
200
  )
201
+ content_start = prompt_str.find(prompt_text)
202
+ if content_start < 0:
203
+ raise ValueError("Classification prompt not found inside chat template")
204
+ conversation_start = content_start + len(prefix)
 
 
 
 
 
 
 
205
 
206
  user_span = f"User: {user_message}"
207
  user_start = conversation_start
 
214
  response_start = user_end
215
  response_end = response_start + len(response_span)
216
 
217
+ trusted_span = None
218
+ if synthetic_response and agent_response is not None:
219
+ trusted_end = response_end - content_start
220
+ trusted_span = (trusted_end - len(agent_response), trusted_end)
221
+ prompt_ids, prompt_offsets = self._encode_chat(
222
+ prompt_str, prompt_text, trusted_span
223
+ )
224
+
225
  prompt_block_ids = []
226
  for start, end in prompt_offsets:
227
  in_user = end > user_start and start < user_end
 
314
  user_message,
315
  rendered_agent_response,
316
  None,
317
+ synthetic_response=not had_response,
318
  )
319
  gen_unsafe_input_ids, gen_unsafe_block_ids = self._build_gen_sequence(
320
  "unsafe",
321
  user_message,
322
  rendered_agent_response,
323
  None,
324
+ synthetic_response=not had_response,
325
  )
326
  hazard_label_mask = int((not is_safe) and bool(violated))
327
  if hazard_label_mask:
 
331
  rendered_agent_response,
332
  None,
333
  violated_categories=violated,
334
+ synthetic_response=not had_response,
335
  )
336
  else:
337
  hazard_input_ids = gen_unsafe_input_ids
pyproject.toml CHANGED
@@ -10,7 +10,7 @@ requires-python = ">=3.10"
10
  license = { text = "MIT" }
11
  dependencies = [
12
  "torch>=2.0",
13
- "transformers>=4.40,<5",
14
  "peft>=0.10",
15
  "accelerate",
16
  "tqdm",
 
10
  license = { text = "MIT" }
11
  dependencies = [
12
  "torch>=2.0",
13
+ "transformers>=4.42,<5",
14
  "peft>=0.10",
15
  "accelerate",
16
  "tqdm",