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
Download llada_guard/data.py from Gertlek/LLaDA-Guard: direct link, hf CLI and curl.
- Browser
- Download file 19.1 kB
-
https://huggingface.co/Gertlek/LLaDA-Guard/resolve/main/llada_guard/data.py
- Command line
-
hf download hf://Gertlek/LLaDA-Guard/llada_guard/data.py
-
curl -L -o data.py https://huggingface.co/Gertlek/LLaDA-Guard/resolve/main/llada_guard/data.py
19.1 kB
| from __future__ import annotations | |
| from functools import partial | |
| from typing import Optional | |
| import torch | |
| from torch.utils.data import DataLoader, Dataset | |
| from transformers import PreTrainedTokenizer | |
| from .prompt import ( | |
| build_prompt, | |
| build_prompt_parts, | |
| build_target, | |
| canonicalize_violated_categories, | |
| ) | |
| PROMPT_BLOCK_ID = 1 | |
| RESPONSE_BLOCK_ID = 2 | |
| MASK_TOKEN_TEXT = "<|mdm_mask|>" | |
| class SafetyDataset(Dataset): | |
| """Materialize the safe/unsafe prompt pairs used by masked-scope scoring.""" | |
| def __init__( | |
| self, | |
| examples: list[dict], | |
| tokenizer: PreTrainedTokenizer, | |
| max_seq_length: int = 2048, | |
| category_augmentation: bool = False, | |
| category_augmentation_prob: float = 0.0, | |
| prompt_only_augmentation_prob: float = 0.0, | |
| prompt_format: str = "aegis_taxonomy", | |
| gen_only: bool = False, | |
| skip_cls: bool = False, | |
| ): | |
| del category_augmentation, category_augmentation_prob, prompt_only_augmentation_prob | |
| del skip_cls | |
| self.examples = examples | |
| self.tokenizer = tokenizer | |
| self.max_seq_length = max_seq_length | |
| self.prompt_format = prompt_format | |
| self.gen_only = gen_only | |
| def __len__(self): | |
| return len(self.examples) | |
| def _chat_token_count( | |
| self, content: str, trusted_span: Optional[tuple[int, int]] = None | |
| ) -> int: | |
| prompt_str = self.tokenizer.apply_chat_template( | |
| [{"role": "user", "content": content}], | |
| add_generation_prompt=True, | |
| tokenize=False, | |
| ) | |
| ids, _ = self._encode_chat(prompt_str, content, trusted_span) | |
| return len(ids) + len(self.tokenizer("")["input_ids"]) | |
| def _encode_chat( | |
| self, | |
| prompt_str: str, | |
| content: str, | |
| trusted_span: Optional[tuple[int, int]] = None, | |
| ) -> tuple[list[int], list[tuple[int, int]]]: | |
| """Tokenize a chat-formatted prompt without parsing special tokens in content. | |
| Special-token strings typed by a user or agent (e.g. ``<|eot_id|>``, | |
| ``<|mdm_mask|>``) stay plain text; only the chat template's own control | |
| tokens are parsed. ``trusted_span`` (offsets within ``content``) marks | |
| release-generated text, such as the synthetic mask response, whose | |
| control tokens are kept. | |
| """ | |
| content_start = prompt_str.find(content) | |
| if content_start < 0: | |
| raise ValueError("Classification prompt not found inside chat template") | |
| content_end = content_start + len(content) | |
| segments = [(0, content_start, False)] | |
| if trusted_span is None: | |
| segments.append((content_start, content_end, True)) | |
| else: | |
| trusted_start = content_start + trusted_span[0] | |
| trusted_end = content_start + trusted_span[1] | |
| segments += [ | |
| (content_start, trusted_start, True), | |
| (trusted_start, trusted_end, False), | |
| (trusted_end, content_end, True), | |
| ] | |
| segments.append((content_end, len(prompt_str), False)) | |
| input_ids: list[int] = [] | |
| offsets: list[tuple[int, int]] = [] | |
| for start, end, split_special in segments: | |
| if start == end: | |
| continue | |
| encoding = self.tokenizer( | |
| prompt_str[start:end], | |
| add_special_tokens=False, | |
| return_offsets_mapping=True, | |
| split_special_tokens=split_special, | |
| ) | |
| input_ids.extend(encoding["input_ids"]) | |
| offsets.extend((s + start, e + start) for s, e in encoding["offset_mapping"]) | |
| return input_ids, offsets | |
| def _gen_total_len( | |
| self, | |
| verdict_text: str, | |
| user_message: str, | |
| agent_response: Optional[str], | |
| categories: Optional[list[str]], | |
| violated_categories: Optional[list[str]] = None, | |
| synthetic_response: bool = False, | |
| ) -> int: | |
| prefix, conversation, suffix = build_prompt_parts( | |
| user_message, | |
| agent_response, | |
| categories=categories, | |
| prompt_format=self.prompt_format, | |
| ) | |
| trusted_span = None | |
| if synthetic_response and agent_response is not None: | |
| trusted_end = len(prefix) + len(conversation) | |
| trusted_span = (trusted_end - len(agent_response), trusted_end) | |
| prompt_len = self._chat_token_count(prefix + conversation + suffix, trusted_span) | |
| if verdict_text == "unsafe" and violated_categories: | |
| label_text = build_target( | |
| False, | |
| violated_categories, | |
| prompt_format=self.prompt_format, | |
| ) | |
| else: | |
| label_text = verdict_text | |
| label_ids = self.tokenizer(label_text, add_special_tokens=False)["input_ids"] | |
| eos_extra = 1 if self.tokenizer.eos_token_id is not None else 0 | |
| return prompt_len + len(label_ids) + eos_extra | |
| def _truncate_to_budget( | |
| self, | |
| user_message: str, | |
| agent_response: Optional[str], | |
| categories: Optional[list[str]], | |
| violated_categories: Optional[list[str]] = None, | |
| allow_agent_drop: bool = True, | |
| ) -> tuple[str, Optional[str]]: | |
| """Trim the response first, then the user text, until the pair fits.""" | |
| while True: | |
| gen_safe_total = self._gen_total_len( | |
| "safe", | |
| user_message, | |
| agent_response, | |
| categories, | |
| synthetic_response=not allow_agent_drop, | |
| ) | |
| gen_unsafe_total = self._gen_total_len( | |
| "unsafe", | |
| user_message, | |
| agent_response, | |
| categories, | |
| violated_categories=violated_categories, | |
| synthetic_response=not allow_agent_drop, | |
| ) | |
| overflow = max(gen_safe_total, gen_unsafe_total) - self.max_seq_length | |
| if overflow <= 0: | |
| return user_message, agent_response | |
| if agent_response and allow_agent_drop: | |
| agent_ids = self.tokenizer(agent_response, add_special_tokens=False)["input_ids"] | |
| drop = min(max(overflow, 1), len(agent_ids)) | |
| agent_ids = agent_ids[:-drop] | |
| agent_response = ( | |
| self.tokenizer.decode(agent_ids, skip_special_tokens=True) if agent_ids else None | |
| ) | |
| continue | |
| user_ids = self.tokenizer(user_message, add_special_tokens=False)["input_ids"] | |
| drop = min(max(overflow, 1), len(user_ids)) | |
| user_ids = user_ids[:-drop] | |
| user_message = self.tokenizer.decode(user_ids, skip_special_tokens=True) if user_ids else "" | |
| def _build_gen_sequence( | |
| self, | |
| verdict_text: str, | |
| user_message: str, | |
| agent_response: Optional[str], | |
| categories: Optional[list[str]], | |
| violated_categories: Optional[list[str]] = None, | |
| synthetic_response: bool = False, | |
| ) -> tuple[torch.Tensor, torch.Tensor]: | |
| prefix, conversation_text, suffix = build_prompt_parts( | |
| user_message, | |
| agent_response, | |
| categories=categories, | |
| prompt_format=self.prompt_format, | |
| ) | |
| prompt_text = prefix + conversation_text + suffix | |
| prompt_str = self.tokenizer.apply_chat_template( | |
| [{"role": "user", "content": prompt_text}], | |
| add_generation_prompt=True, | |
| tokenize=False, | |
| ) | |
| content_start = prompt_str.find(prompt_text) | |
| if content_start < 0: | |
| raise ValueError("Classification prompt not found inside chat template") | |
| conversation_start = content_start + len(prefix) | |
| user_span = f"User: {user_message}" | |
| user_start = conversation_start | |
| user_end = user_start + len(user_span) | |
| response_start = None | |
| response_end = None | |
| if agent_response is not None: | |
| response_span = f"\nAgent: {agent_response}" | |
| response_start = user_end | |
| response_end = response_start + len(response_span) | |
| trusted_span = None | |
| if synthetic_response and agent_response is not None: | |
| trusted_end = response_end - content_start | |
| trusted_span = (trusted_end - len(agent_response), trusted_end) | |
| prompt_ids, prompt_offsets = self._encode_chat( | |
| prompt_str, prompt_text, trusted_span | |
| ) | |
| prompt_block_ids = [] | |
| for start, end in prompt_offsets: | |
| in_user = end > user_start and start < user_end | |
| in_response = ( | |
| response_start is not None | |
| and response_end is not None | |
| and end > response_start | |
| and start < response_end | |
| ) | |
| if in_user: | |
| prompt_block_ids.append(PROMPT_BLOCK_ID) | |
| elif in_response: | |
| prompt_block_ids.append(RESPONSE_BLOCK_ID) | |
| else: | |
| prompt_block_ids.append(0) | |
| include_cats = verdict_text == "unsafe" and violated_categories | |
| if include_cats: | |
| label_text = build_target( | |
| False, | |
| violated_categories, | |
| prompt_format=self.prompt_format, | |
| ) | |
| else: | |
| label_text = verdict_text | |
| label_encoding = self.tokenizer( | |
| label_text, | |
| add_special_tokens=False, | |
| return_offsets_mapping=True, | |
| ) | |
| label_ids = label_encoding["input_ids"] | |
| label_offsets = label_encoding["offset_mapping"] | |
| if include_cats: | |
| char_blocks = [0] * len(label_text) | |
| pos = len("unsafe") + 1 | |
| for cat_idx, cat in enumerate(violated_categories): | |
| for offset in range(len(cat)): | |
| if pos + offset < len(char_blocks): | |
| char_blocks[pos + offset] = 3 + cat_idx | |
| pos += len(cat) + 1 | |
| label_block_ids = [] | |
| for start, end in label_offsets: | |
| block_id = 0 | |
| for char_idx in range(start, min(end, len(char_blocks))): | |
| block_id = max(block_id, char_blocks[char_idx]) | |
| label_block_ids.append(block_id) | |
| else: | |
| label_block_ids = [0] * len(label_ids) | |
| input_ids = list(prompt_ids) + list(label_ids) | |
| block_ids = prompt_block_ids + label_block_ids | |
| eos_id = self.tokenizer.eos_token_id | |
| if eos_id is not None: | |
| input_ids.append(eos_id) | |
| block_ids.append(0) | |
| return ( | |
| torch.tensor(input_ids, dtype=torch.long), | |
| torch.tensor(block_ids, dtype=torch.long), | |
| ) | |
| def __getitem__(self, idx): | |
| ex = self.examples[idx] | |
| user_message = str(ex["user_message"]) | |
| agent_response = ex.get("agent_response") | |
| is_safe = bool(ex.get("is_safe", True)) | |
| violated = canonicalize_violated_categories(ex.get("violated_categories", [])) | |
| had_response = agent_response is not None | |
| rendered_agent_response = str(agent_response) if had_response else MASK_TOKEN_TEXT | |
| user_message, rendered_agent_response = self._truncate_to_budget( | |
| user_message, | |
| rendered_agent_response, | |
| None, | |
| violated_categories=violated, | |
| allow_agent_drop=had_response, | |
| ) | |
| has_response = int(had_response and rendered_agent_response is not None) | |
| prompt_text = build_prompt( | |
| user_message, | |
| rendered_agent_response, | |
| prompt_format=self.prompt_format, | |
| ) | |
| gen_safe_input_ids, gen_safe_block_ids = self._build_gen_sequence( | |
| "safe", | |
| user_message, | |
| rendered_agent_response, | |
| None, | |
| synthetic_response=not had_response, | |
| ) | |
| gen_unsafe_input_ids, gen_unsafe_block_ids = self._build_gen_sequence( | |
| "unsafe", | |
| user_message, | |
| rendered_agent_response, | |
| None, | |
| synthetic_response=not had_response, | |
| ) | |
| hazard_label_mask = int((not is_safe) and bool(violated)) | |
| if hazard_label_mask: | |
| hazard_input_ids, hazard_block_ids = self._build_gen_sequence( | |
| "unsafe", | |
| user_message, | |
| rendered_agent_response, | |
| None, | |
| violated_categories=violated, | |
| synthetic_response=not had_response, | |
| ) | |
| else: | |
| hazard_input_ids = gen_unsafe_input_ids | |
| hazard_block_ids = gen_unsafe_block_ids | |
| prompt_label = (ex.get("prompt_label") or "").strip().lower() | |
| response_label = (ex.get("response_label") or "").strip().lower() | |
| prompt_label_known = prompt_label in {"safe", "unsafe"} | |
| response_label_known = had_response and response_label in {"safe", "unsafe"} | |
| if not prompt_label_known and not had_response: | |
| prompt_label_known = True | |
| if not response_label_known and had_response: | |
| response_label_known = True | |
| prompt_is_unsafe = ( | |
| int(prompt_label == "unsafe") | |
| if prompt_label in {"safe", "unsafe"} | |
| else int((not is_safe) and not had_response) | |
| ) | |
| response_is_unsafe = ( | |
| int(response_label == "unsafe") | |
| if response_label in {"safe", "unsafe"} | |
| else int((not is_safe) and had_response) | |
| ) | |
| return { | |
| "gen_safe_input_ids": gen_safe_input_ids, | |
| "gen_safe_block_ids": gen_safe_block_ids, | |
| "gen_safe_real_length": len(gen_safe_input_ids), | |
| "gen_unsafe_input_ids": gen_unsafe_input_ids, | |
| "gen_unsafe_block_ids": gen_unsafe_block_ids, | |
| "gen_unsafe_real_length": len(gen_unsafe_input_ids), | |
| "hazard_input_ids": hazard_input_ids, | |
| "hazard_block_ids": hazard_block_ids, | |
| "hazard_real_length": len(hazard_input_ids), | |
| "hazard_label_mask": hazard_label_mask, | |
| "hazard_eval_mask": int(is_safe or bool(violated)), | |
| "hazard_targets": list(violated), | |
| "prompt_text": prompt_text, | |
| "is_unsafe": int(not is_safe), | |
| "conv_label_mask": int(ex.get("conv_label_mask", 1)), | |
| "prompt_is_unsafe": prompt_is_unsafe, | |
| "prompt_label_mask": int(prompt_label_known), | |
| "response_is_unsafe": response_is_unsafe, | |
| "response_label_mask": int(response_label_known), | |
| "has_response": has_response, | |
| "example_index": int(ex.get("example_index", idx)), | |
| "eval_position": int(ex.get("eval_position", -1)), | |
| } | |
| def _pad_tensor_batch( | |
| batch: list[dict], | |
| ids_key: str, | |
| blocks_key: str, | |
| length_key: str, | |
| pad_id: int, | |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]: | |
| max_len = max(item[length_key] for item in batch) | |
| ids_list = [] | |
| block_list = [] | |
| lengths = [] | |
| for item in batch: | |
| ids = item[ids_key] | |
| blocks = item[blocks_key] | |
| pad_len = max_len - item[length_key] | |
| ids_list.append(torch.cat([ids, torch.full((pad_len,), pad_id, dtype=torch.long)])) | |
| block_list.append(torch.cat([blocks, torch.zeros(pad_len, dtype=torch.long)])) | |
| lengths.append(item[length_key]) | |
| return ( | |
| torch.stack(ids_list), | |
| torch.stack(block_list), | |
| torch.tensor(lengths, dtype=torch.long), | |
| ) | |
| def collate_fn( | |
| batch, | |
| pad_id: int = 0, | |
| mask_id: int = 126336, | |
| gen_only: bool = False, | |
| skip_cls: bool = False, | |
| ): | |
| """Collate materialized generative sequence views.""" | |
| del mask_id, gen_only, skip_cls | |
| gen_safe_ids, gen_safe_blocks, gen_safe_lengths = _pad_tensor_batch( | |
| batch, | |
| "gen_safe_input_ids", | |
| "gen_safe_block_ids", | |
| "gen_safe_real_length", | |
| pad_id, | |
| ) | |
| gen_unsafe_ids, gen_unsafe_blocks, gen_unsafe_lengths = _pad_tensor_batch( | |
| batch, | |
| "gen_unsafe_input_ids", | |
| "gen_unsafe_block_ids", | |
| "gen_unsafe_real_length", | |
| pad_id, | |
| ) | |
| hazard_ids, hazard_blocks, hazard_lengths = _pad_tensor_batch( | |
| batch, | |
| "hazard_input_ids", | |
| "hazard_block_ids", | |
| "hazard_real_length", | |
| pad_id, | |
| ) | |
| return { | |
| "gen_safe_input_ids": gen_safe_ids, | |
| "gen_safe_block_ids": gen_safe_blocks, | |
| "gen_safe_real_lengths": gen_safe_lengths, | |
| "gen_unsafe_input_ids": gen_unsafe_ids, | |
| "gen_unsafe_block_ids": gen_unsafe_blocks, | |
| "gen_unsafe_real_lengths": gen_unsafe_lengths, | |
| "hazard_input_ids": hazard_ids, | |
| "hazard_block_ids": hazard_blocks, | |
| "hazard_real_lengths": hazard_lengths, | |
| "hazard_label_mask": torch.tensor( | |
| [item.get("hazard_label_mask", 0) for item in batch], | |
| dtype=torch.float32, | |
| ), | |
| "is_unsafe": torch.tensor([item["is_unsafe"] for item in batch], dtype=torch.float32), | |
| "conv_label_mask": torch.tensor( | |
| [item.get("conv_label_mask", 1) for item in batch], | |
| dtype=torch.float32, | |
| ), | |
| "has_response": torch.tensor([item["has_response"] for item in batch], dtype=torch.float32), | |
| "example_index": torch.tensor( | |
| [item.get("example_index", -1) for item in batch], | |
| dtype=torch.long, | |
| ), | |
| "prompt_is_unsafe": torch.tensor( | |
| [item.get("prompt_is_unsafe", 0) for item in batch], | |
| dtype=torch.float32, | |
| ), | |
| "prompt_label_mask": torch.tensor( | |
| [item.get("prompt_label_mask", 0) for item in batch], | |
| dtype=torch.float32, | |
| ), | |
| "response_is_unsafe": torch.tensor( | |
| [item.get("response_is_unsafe", 0) for item in batch], | |
| dtype=torch.float32, | |
| ), | |
| "response_label_mask": torch.tensor( | |
| [item.get("response_label_mask", 0) for item in batch], | |
| dtype=torch.float32, | |
| ), | |
| } | |
| def build_eval_loader( | |
| examples: list[dict], | |
| tokenizer, | |
| max_seq_length: int, | |
| batch_size: int, | |
| *, | |
| gen_only: bool = True, | |
| prompt_format: str = "aegis_taxonomy", | |
| ) -> DataLoader: | |
| """Build the deterministic eval loader used by public inference.""" | |
| dataset = SafetyDataset( | |
| examples, | |
| tokenizer, | |
| max_seq_length=max_seq_length, | |
| category_augmentation=False, | |
| prompt_format=prompt_format, | |
| gen_only=gen_only, | |
| ) | |
| items = [dataset[idx] for idx in range(len(dataset))] | |
| items.sort( | |
| key=lambda it: ( | |
| int(it.get("has_response", 0)), | |
| max(it["gen_safe_real_length"], it["gen_unsafe_real_length"]), | |
| ) | |
| ) | |
| pad_id = tokenizer.eos_token_id or 0 | |
| return DataLoader( | |
| items, | |
| batch_size=batch_size, | |
| shuffle=False, | |
| collate_fn=partial(collate_fn, pad_id=pad_id, gen_only=gen_only), | |
| num_workers=0, | |
| pin_memory=torch.cuda.is_available(), | |
| ) | |