| import os |
|
|
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") |
|
|
| import spaces |
| import torch |
| import gradio as gr |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| from inverse_segmentation import GemmaInverseSegmenter |
|
|
| MODEL_ID = "dsba-lab/gemma2-9b-it-alienlm-full" |
| BASE_TOKENIZER_ID = "google/gemma-2-9b-it" |
| ALIEN_TOKENIZER_PATH = "assets/alien_tokenizer" |
|
|
| alien_tokenizer = AutoTokenizer.from_pretrained( |
| ALIEN_TOKENIZER_PATH, |
| use_fast=True, |
| ) |
| base_tokenizer = AutoTokenizer.from_pretrained( |
| BASE_TOKENIZER_ID, |
| use_fast=True, |
| token=os.environ.get("HF_TOKEN"), |
| ) |
|
|
|
|
| def _vocab_id_range(tokenizer) -> tuple[int, int]: |
| ids = tokenizer.get_vocab().values() |
| return min(ids), max(ids) |
|
|
|
|
| def _assert_tokenizer_compatibility() -> None: |
| """Apply the compatibility checks used by the official translator.""" |
| if alien_tokenizer.__class__.__name__ != base_tokenizer.__class__.__name__: |
| raise RuntimeError("Alien and base tokenizer classes do not match.") |
| if _vocab_id_range(alien_tokenizer) != _vocab_id_range(base_tokenizer): |
| raise RuntimeError("Alien and base tokenizer ID ranges do not match.") |
| if len(alien_tokenizer) != len(base_tokenizer): |
| raise RuntimeError("Alien and base tokenizer vocabulary sizes do not match.") |
|
|
|
|
| _assert_tokenizer_compatibility() |
|
|
| |
| |
| |
| inverse_segmenter = GemmaInverseSegmenter(base_tokenizer, alien_tokenizer) |
|
|
| |
| model = AutoModelForCausalLM.from_pretrained( |
| MODEL_ID, |
| dtype=torch.bfloat16, |
| token=os.environ.get("HF_TOKEN"), |
| ).to("cuda") |
| model.eval() |
|
|
|
|
| def plain2alien(text: str) -> str: |
| token_ids = base_tokenizer.encode(text, add_special_tokens=False) |
| return alien_tokenizer.decode( |
| token_ids, |
| skip_special_tokens=False, |
| clean_up_tokenization_spaces=False, |
| ) |
|
|
|
|
| def alien2plain(text: str) -> str: |
| token_ids = alien_tokenizer.encode(text, add_special_tokens=False) |
| return base_tokenizer.decode( |
| token_ids, |
| skip_special_tokens=False, |
| clean_up_tokenization_spaces=False, |
| ) |
|
|
|
|
| @spaces.GPU(duration=120) |
| def run_alienlm( |
| user_prompt: str, |
| max_new_tokens: int = 128, |
| temperature: float = 0.7, |
| top_p: float = 0.9, |
| progress=gr.Progress(track_tqdm=True), |
| ): |
| """Run the full AlienLM privacy pipeline. |
| |
| 1. Translate user's natural-language prompt into alien text. |
| 2. Feed the alienized prompt to the AlienLM-adapted model. |
| 3. Decode the model's alien-language response. |
| 4. Translate the alien response back into natural text. |
| |
| Args: |
| user_prompt: Natural-language input from the user. |
| max_new_tokens: Maximum number of new tokens to generate. |
| temperature: Sampling temperature for generation. |
| top_p: Nucleus-sampling probability threshold. |
| """ |
| if not user_prompt.strip(): |
| return "", "", "", "Please enter a prompt." |
|
|
| |
| alien_prompt = plain2alien(user_prompt) |
|
|
| |
| |
| |
| messages = [{"role": "user", "content": user_prompt}] |
| plain_input_text = base_tokenizer.apply_chat_template( |
| messages, |
| tokenize=False, |
| add_generation_prompt=True, |
| ) |
| alien_input_text = plain2alien(plain_input_text) |
| inputs = base_tokenizer( |
| alien_input_text, |
| add_special_tokens=False, |
| return_tensors="pt", |
| ).to("cuda") |
|
|
| |
| with torch.no_grad(): |
| output_ids = model.generate( |
| **inputs, |
| max_new_tokens=max_new_tokens, |
| do_sample=temperature > 0, |
| temperature=max(temperature, 1e-5), |
| top_p=top_p, |
| pad_token_id=( |
| alien_tokenizer.pad_token_id |
| if alien_tokenizer.pad_token_id is not None |
| else alien_tokenizer.eos_token_id |
| ), |
| ) |
|
|
| |
| generated_ids = output_ids[0][inputs["input_ids"].shape[1]:] |
| |
| alien_response = base_tokenizer.decode( |
| generated_ids, |
| skip_special_tokens=True, |
| clean_up_tokenization_spaces=False, |
| ) |
|
|
| |
| |
| |
| try: |
| recovery = inverse_segmenter.recover(alien_response) |
| recovered_response = recovery.text |
| except ValueError: |
| |
| |
| recovered_response = alien2plain(alien_response) |
|
|
| return alien_prompt, alien_response, recovered_response, "" |
|
|
|
|
| |
|
|
| CSS = """ |
| #col-container { max-width: 1100px; margin: 0 auto; } |
| .dark .gradio-container { color: var(--body-text-color); } |
| """ |
|
|
| with gr.Blocks() as demo: |
| with gr.Column(elem_id="col-container"): |
| gr.Markdown( |
| "# 🛸 AlienLM: Alienization of Language for API-Boundary Privacy\n" |
| "Type a prompt in natural language. It gets translated into an *alien language* " |
| "via a vocabulary-level token bijection, processed by an AlienLM-adapted Gemma-2-9B model, " |
| "and the alien response is translated back to natural text — all without the model ever " |
| "seeing your original words.\n\n" |
| "📄 [Paper](https://arxiv.org/abs/2601.22710) · " |
| "💻 [Code](https://github.com/KimJaehee0725/AlienLM) · " |
| "🤗 [Model](https://huggingface.co/dsba-lab/gemma2-9b-it-alienlm-full)\n\n" |
| "> **Demo scope:** This Space runs translation and inference in one backend process. " |
| "In the intended deployment, the translator and bijection remain on the trusted client, " |
| "so only alienized text crosses the model API boundary." |
| ) |
|
|
| with gr.Row(): |
| prompt = gr.Textbox( |
| label="Your prompt (natural language)", |
| placeholder="e.g. Write a short poem about the ocean.", |
| lines=3, |
| scale=4, |
| ) |
| run_btn = gr.Button("Run Pipeline", variant="primary", scale=1) |
|
|
| with gr.Accordion("Advanced settings", open=False): |
| max_tokens = gr.Slider(32, 256, value=128, step=16, label="Max new tokens") |
| temp = gr.Slider(0.0, 2.0, value=0.7, step=0.05, label="Temperature") |
| top_p_val = gr.Slider(0.1, 1.0, value=0.9, step=0.05, label="Top-p") |
|
|
| with gr.Tabs(): |
| with gr.Tab("Recovered Response"): |
| recovered_out = gr.Textbox( |
| label="Natural-language response (alien → plain)", |
| lines=8, |
| interactive=False, |
| ) |
| with gr.Tab("Alienized Prompt"): |
| alien_prompt_out = gr.Textbox( |
| label="Alienized prompt (what the model actually sees)", |
| lines=4, |
| interactive=False, |
| ) |
| with gr.Tab("Alien Response"): |
| alien_response_out = gr.Textbox( |
| label="Raw alien response from the model", |
| lines=8, |
| interactive=False, |
| ) |
|
|
| error_box = gr.Textbox(label="Status", visible=False) |
|
|
| gr.Examples( |
| examples=[ |
| ["Write a short poem about the ocean."], |
| ["Explain quantum computing to a child."], |
| ["What is the meaning of life?"], |
| ["Give me three tips for staying healthy."], |
| ], |
| inputs=[prompt], |
| outputs=[alien_prompt_out, alien_response_out, recovered_out, error_box], |
| fn=run_alienlm, |
| cache_examples=True, |
| cache_mode="lazy", |
| ) |
|
|
| run_btn.click( |
| fn=run_alienlm, |
| inputs=[prompt, max_tokens, temp, top_p_val], |
| outputs=[alien_prompt_out, alien_response_out, recovered_out, error_box], |
| ) |
|
|
| prompt.submit( |
| fn=run_alienlm, |
| inputs=[prompt, max_tokens, temp, top_p_val], |
| outputs=[alien_prompt_out, alien_response_out, recovered_out, error_box], |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| demo.launch(mcp_server=True, theme=gr.themes.Citrus(), css=CSS) |
|
|