| """Prompt-injection classifier on top of the axiotic/ogma-base encoder. |
| |
| Loadable with AutoModelForSequenceClassification / pipeline via trust_remote_code. |
| |
| The base encoder declares its rotary position caches as non-persistent buffers, |
| so a vanilla from_pretrained leaves them uninitialised (NaN/zeros, nondeterministic |
| per process). We rebuild them from inv_freq AND re-register them as PERSISTENT, so |
| the correct values are saved into this model's weights and loaded reliably — the |
| NaN bug cannot reach downstream users. |
| """ |
| import torch |
| from torch import nn |
| from transformers import PreTrainedModel, AutoModel, AutoConfig |
| from transformers.modeling_outputs import SequenceClassifierOutput |
|
|
| try: |
| from .configuration_ogma_classifier import OgmaClassifierConfig |
| except ImportError: |
| from configuration_ogma_classifier import OgmaClassifierConfig |
|
|
|
|
| class OgmaForPromptInjection(PreTrainedModel): |
| config_class = OgmaClassifierConfig |
|
|
| def __init__(self, config): |
| super().__init__(config) |
| base_cfg = AutoConfig.from_pretrained(config.base_model_id, trust_remote_code=True) |
| self.encoder = AutoModel.from_config(base_cfg, trust_remote_code=True) |
| self.dropout = nn.Dropout(0.1) |
| self.head = nn.Linear(config.hidden, config.num_labels) |
| self._rope_ready = False |
| self.post_init() |
|
|
| def _ensure_rope(self): |
| """Rebuild the encoder's rotary caches from inv_freq. The base declares |
| cos/sin as non-persistent buffers, so from_pretrained leaves them |
| uninitialised; rebuilding here makes inference correct and deterministic. |
| Done lazily on first forward so it survives any instantiation path |
| (from_config, meta/fast-init, etc.). |
| |
| The encoder's _build_cache builds its position grid on CPU (torch.arange |
| with no device), so a rebuild while the module sits on MPS/CUDA raises a |
| device-mismatch. Rebuild each tiny RoPE module on CPU, then move it back |
| to its original device — correct on every backend, not just CPU.""" |
| for m in self.modules(): |
| if hasattr(m, "_build_cache") and hasattr(m, "cos_cached"): |
| dev = m.cos_cached.device |
| m.to("cpu") |
| m._build_cache(m.cos_cached.shape[0]) |
| m.to(dev) |
| self._rope_ready = True |
|
|
| def forward(self, input_ids, attention_mask=None, labels=None, **kwargs): |
| if not self._rope_ready: |
| self._ensure_rope() |
| emb = self.encoder(input_ids=input_ids, attention_mask=attention_mask) |
| logits = self.head(self.dropout(emb)) |
| loss = None |
| if labels is not None: |
| loss = nn.functional.cross_entropy(logits, labels) |
| return SequenceClassifierOutput(loss=loss, logits=logits) |
|
|