| """ |
| Modal app: CoPA (Contrastive Paraphrase Attack) on GPU. |
| |
| Deploys the CoPA prototype to Modal cloud — no local torch needed. |
| Dry-run mode: just validates the pipeline without launching real GPU. |
| |
| Usage: |
| # Dry-run (local validation, 0€) |
| modal run src/modal_app_copa.py --dry-run |
| |
| # Real run on T4 GPU (~0.60€/h) |
| modal run src/modal_app_copa.py |
| |
| # Real run on A100 80GB (~2.50€/h) |
| modal run src/modal_app_copa.py --gpu A100-80GB |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| import os |
| import sys |
| import time |
| from dataclasses import asdict, dataclass |
|
|
| import modal |
|
|
| |
| |
| |
|
|
| image = ( |
| modal.Image.debian_slim(python_version="3.12") |
| .env({"PIP_PROGRESS_BAR": "off"}) |
| .pip_install( |
| "torch>=2.4.0", |
| "transformers>=4.45.0", |
| "accelerate>=0.34.0", |
| "numpy>=1.26.0", |
| ) |
| ) |
|
|
| app = modal.App("evasion-detection-copa", image=image) |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class CostGuard: |
| max_runtime_minutes: int = 30 |
| max_cost_eur: float = 5.0 |
| gpu_type: str = "T4" |
| dry_run: bool = True |
|
|
| def validate(self, elapsed_m: float) -> bool: |
| rates = {"T4": 0.60, "L4": 0.80, "A10G": 1.10, "A100-80GB": 2.50, "H100": 3.95} |
| rate = rates.get(self.gpu_type, 2.50) |
| if elapsed_m > self.max_runtime_minutes: |
| print(f"[CostGuard] TIMEOUT: {elapsed_m:.0f}m > {self.max_runtime_minutes}m") |
| return False |
| cost = (elapsed_m / 60) * rate |
| if cost > self.max_cost_eur: |
| print(f"[CostGuard] OVER BUDGET: {cost:.2f}€ > {self.max_cost_eur}€") |
| return False |
| return True |
|
|
|
|
| |
| |
| |
|
|
| def _run_copa_on_gpu( |
| texts: list[str], |
| model_name: str = "Qwen/Qwen2.5-1.5B-Instruct", |
| lambda_contrast: float = 0.8, |
| alpha_truncation: float = 1e-5, |
| max_new_tokens: int = 768, |
| temperature: float = 1.0, |
| top_p: float = 0.92, |
| repetition_penalty: float = 1.15, |
| diversity_bonus_strength: float = 0.5, |
| human_style_prompt: str = "", |
| machine_style_prompt: str = "", |
| dry_run: bool = False, |
| ) -> dict: |
| """Core CoPA rewriting — executed inside Modal GPU container.""" |
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| if dry_run: |
| return { |
| "status": "dry_run", |
| "num_samples": len(texts), |
| "model": model_name, |
| "message": "Dry-run: GPU not used, no text generated.", |
| } |
|
|
| print(f"[CoPA-Modal] Loading {model_name}...") |
| tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) |
| if tokenizer.pad_token is None: |
| tokenizer.pad_token = tokenizer.eos_token |
|
|
| model = AutoModelForCausalLM.from_pretrained( |
| model_name, |
| torch_dtype=torch.float16, |
| device_map="auto", |
| trust_remote_code=True, |
| ) |
| model.eval() |
|
|
| if human_style_prompt == "": |
| human_style_prompt = ( |
| "Rewrite this to sound like a natural human wrote it, " |
| "with varied sentences and conversational wording:\n\n{input_text}" |
| ) |
| if machine_style_prompt == "": |
| machine_style_prompt = ( |
| "Repeat the following text exactly, word for word, " |
| "maintaining the original formal structure:\n\n{input_text}" |
| ) |
|
|
| results = [] |
| total_tokens = 0 |
| start = time.time() |
|
|
| for i, text in enumerate(texts): |
| |
| h_text = human_style_prompt.replace("{input_text}", text) |
| m_text = machine_style_prompt.replace("{input_text}", text) |
|
|
| h_in = tokenizer(h_text, return_tensors="pt").to(model.device) |
| m_in = tokenizer(m_text, return_tensors="pt").to(model.device) |
|
|
| generated = [] |
| with torch.no_grad(): |
| for step in range(max_new_tokens): |
| h_logits = model(**h_in).logits[0, -1, :] / temperature |
| m_logits = model(**m_in).logits[0, -1, :] / temperature |
|
|
| |
| h_lp = torch.log_softmax(h_logits, dim=-1) |
| m_lp = torch.log_softmax(m_logits, dim=-1) |
| contrastive = (1 + lambda_contrast) * h_lp - lambda_contrast * m_lp |
|
|
| |
| if repetition_penalty != 1.0 and generated: |
| for gid in set(generated): |
| if contrastive[gid] > 0: |
| contrastive[gid] /= repetition_penalty |
| else: |
| contrastive[gid] *= repetition_penalty |
|
|
| |
| h_probs = torch.softmax(h_logits, dim=-1) |
| mask = h_probs >= alpha_truncation * h_probs.max() |
| contrastive[~mask] = float("-inf") |
|
|
| |
| if top_p < 1.0: |
| sorted_logits, sorted_indices = torch.sort(contrastive, descending=True) |
| cumulative_probs = torch.cumsum(torch.softmax(sorted_logits, dim=-1), dim=-1) |
| sorted_indices_to_remove = cumulative_probs > top_p |
| sorted_indices_to_remove[0] = False |
| indices_to_remove = sorted_indices[sorted_indices_to_remove] |
| contrastive[indices_to_remove] = float("-inf") |
|
|
| |
| if diversity_bonus_strength > 0 and len(generated) >= 3: |
| recent_window = generated[-20:] |
| for gid in set(recent_window): |
| contrastive[gid] -= diversity_bonus_strength * recent_window.count(gid) |
|
|
| probs = torch.softmax(contrastive, dim=-1) |
| next_id = torch.multinomial(probs, 1).item() |
| generated.append(next_id) |
|
|
| |
| h_in["input_ids"] = torch.cat( |
| [h_in["input_ids"], torch.tensor([[next_id]], device=model.device)], dim=1 |
| ) |
| h_in["attention_mask"] = torch.ones_like(h_in["input_ids"]) |
| m_in["input_ids"] = torch.cat( |
| [m_in["input_ids"], torch.tensor([[next_id]], device=model.device)], dim=1 |
| ) |
| m_in["attention_mask"] = torch.ones_like(m_in["input_ids"]) |
|
|
| if next_id == tokenizer.eos_token_id: |
| break |
|
|
| rewritten = tokenizer.decode(generated, skip_special_tokens=True) |
|
|
| |
| import re |
| cut_patterns = [ |
| r"\n\s*Text:\s", r"\n\s*Human version:", r"\n\s*Formal", |
| r"###\s", r"\n\s*You are an AI", |
| ] |
| for pattern in cut_patterns: |
| m = re.search(pattern, rewritten) |
| if m: |
| rewritten = rewritten[: m.start()].strip() |
| break |
| |
| if rewritten and rewritten[-1] not in '.!?': |
| last_period = max(rewritten.rfind('.'), rewritten.rfind('!'), rewritten.rfind('?')) |
| if last_period > len(rewritten) * 0.6: |
| rewritten = rewritten[: last_period + 1] |
|
|
| results.append({"original": text, "rewritten": rewritten.strip(), "tokens": len(generated)}) |
| total_tokens += len(generated) |
| if (i + 1) % 10 == 0: |
| print(f"[CoPA-Modal] {i+1}/{len(texts)} done") |
|
|
| elapsed = time.time() - start |
| print(f"[CoPA-Modal] Done: {len(results)} samples, {total_tokens} tokens, {elapsed:.1f}s") |
|
|
| return { |
| "status": "completed", |
| "num_samples": len(results), |
| "total_tokens": total_tokens, |
| "elapsed_seconds": elapsed, |
| "tokens_per_second": total_tokens / elapsed if elapsed > 0 else 0, |
| "config": { |
| "model": model_name, |
| "lambda_contrast": lambda_contrast, |
| "top_p": top_p, |
| "repetition_penalty": repetition_penalty, |
| "diversity_bonus": diversity_bonus_strength, |
| "max_new_tokens": max_new_tokens, |
| }, |
| "results": results, |
| } |
|
|
|
|
| |
| |
| |
|
|
| @app.function( |
| gpu=os.getenv("MODAL_COPA_GPU", "T4"), |
| timeout=60 * 30, |
| scaledown_window=60 * 5, |
| ) |
| def copa_rewrite_modal( |
| texts: list[str], |
| model_name: str = "Qwen/Qwen2.5-1.5B-Instruct", |
| lambda_contrast: float = 0.8, |
| alpha_truncation: float = 1e-5, |
| max_new_tokens: int = 768, |
| temperature: float = 1.0, |
| top_p: float = 0.92, |
| repetition_penalty: float = 1.15, |
| diversity_bonus_strength: float = 0.5, |
| dry_run: bool = False, |
| ) -> dict: |
| """Run CoPA rewriting on Modal GPU.""" |
| guard = CostGuard( |
| gpu_type=os.getenv("MODAL_COPA_GPU", "T4"), |
| dry_run=dry_run, |
| ) |
| return _run_copa_on_gpu( |
| texts=texts, |
| model_name=model_name, |
| lambda_contrast=lambda_contrast, |
| alpha_truncation=alpha_truncation, |
| max_new_tokens=max_new_tokens, |
| temperature=temperature, |
| top_p=top_p, |
| repetition_penalty=repetition_penalty, |
| diversity_bonus_strength=diversity_bonus_strength, |
| dry_run=dry_run, |
| ) |
|
|
|
|
| |
| |
| |
|
|
| @app.local_entrypoint() |
| def main( |
| num_samples: int = 10, |
| model_name: str = "Qwen/Qwen2.5-1.5B-Instruct", |
| gpu: str = "T4", |
| dry_run: bool = False, |
| text: str = "", |
| text_file: str = "", |
| ): |
| """Local entrypoint — builds test texts and dispatches to Modal GPU. |
| |
| Use --text "Your custom text here" for single-text rewrite. |
| Use --text-file path/to/file.txt to load from a file. |
| """ |
| os.environ["MODAL_COPA_GPU"] = gpu |
|
|
| |
| if text: |
| texts = [text] |
| print(f"[Modal-Copa] Custom text mode: {len(text[0].split())} words") |
| elif text_file: |
| with open(text_file, "r", encoding="utf-8") as f: |
| content = f.read().strip() |
| texts = [content] |
| print(f"[Modal-Copa] Loaded text from {text_file}: {len(content.split())} words") |
| else: |
| |
| templates = [ |
| "Artificial intelligence has revolutionized natural language processing in recent years. The development of large language models has enabled unprecedented capabilities in text generation, translation, and summarization tasks.", |
| "Climate change represents one of the most significant challenges facing humanity. Rising global temperatures have led to increasingly severe weather events and disruptions to ecosystems worldwide.", |
| "The history of computer science can be traced back to the foundational work of Alan Turing. His theoretical contributions laid the groundwork for the digital revolution that followed.", |
| "Machine learning algorithms have demonstrated remarkable success across a wide range of applications. These systems learn patterns from large datasets to make accurate predictions.", |
| "The Renaissance period marked a profound transformation in European art, science, and philosophy. This cultural movement began in Italy during the fourteenth century.", |
| ] |
| texts = [templates[i % len(templates)] for i in range(num_samples)] |
|
|
| print(f"[Modal-Copa] Dispatching {len(texts)} samples to Modal GPU={gpu}") |
| print(f"[Modal-Copa] Model: {model_name}") |
| print(f"[Modal-Copa] Dry-run: {dry_run}") |
|
|
| if dry_run: |
| result = { |
| "status": "dry_run_validated", |
| "num_samples": len(texts), |
| "model": model_name, |
| "gpu": gpu, |
| "message": "Dry-run: Modal image built and ready. Run without --dry-run for real GPU execution.", |
| } |
| else: |
| result = copa_rewrite_modal.remote( |
| texts=texts, |
| model_name=model_name, |
| dry_run=False, |
| ) |
|
|
| |
| os.makedirs("output", exist_ok=True) |
| out_path = "output/copa_modal_results.json" |
| with open(out_path, "w", encoding="utf-8") as f: |
| json.dump(result, f, indent=2, ensure_ascii=False) |
|
|
| print(f"[Modal-Copa] Results saved to {out_path}") |
| print(f"[Modal-Copa] Status: {result.get('status')}") |
|
|
| if result.get("status") == "completed": |
| for i, r in enumerate(result["results"][:3]): |
| print(f"\n--- Sample {i+1} ---") |
| print(f" Original ({len(r['original'].split())}w): {r['original'][:120]}...") |
| |
| safe_text = r['rewritten'][:300].encode('ascii', errors='replace').decode('ascii') |
| print(f" Rewritten ({r['tokens']}t): {safe_text}") |
|
|