Instructions to use Gertlek/LLaDA-Guard with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use Gertlek/LLaDA-Guard with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Special token bug fix
Browse files- llada_guard/data.py +86 -21
- 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(
|
|
|
|
|
|
|
| 49 |
prompt_str = self.tokenizer.apply_chat_template(
|
| 50 |
[{"role": "user", "content": content}],
|
| 51 |
add_generation_prompt=True,
|
| 52 |
tokenize=False,
|
| 53 |
)
|
| 54 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 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.
|
| 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",
|