Skip to content

Commit 48263b8

Browse files
committed
fix(docker): vendor a non-interactive PIGuard ONNX exporter
Stop patching the upstream export script with sed. Instead, ship a local export_onnx.py derived from the guard repo with trust_remote_code=True baked in. The download script mounts this file into the Python container and runs it directly. This avoids the broken multi-line sed replacement and the interactive HF prompt.
1 parent 1b684e8 commit 48263b8

3 files changed

Lines changed: 91 additions & 32 deletions

File tree

3.26 KB
Binary file not shown.

docker/piguard/download-model.sh

Lines changed: 16 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -2,71 +2,55 @@
22
# Download the PIGuard ONNX model and tokenizer into docker/piguard/models/.
33
# This is a one-time step required before the PIGuard sidecar can start in
44
# Docker Compose. The exported model is ~735 MB.
5+
#
6+
# The export script in this directory is derived from the go-prompt-injection-guard
7+
# export script with trust_remote_code=True added so it can run non-interactively.
58

69
set -euo pipefail
710

811
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
912
MODEL_DIR="${SCRIPT_DIR}/models"
13+
EXPORT_SCRIPT="${SCRIPT_DIR}/export_onnx.py"
1014
mkdir -p "${MODEL_DIR}"
1115

1216
if [ -f "${MODEL_DIR}/model.onnx" ] && [ -f "${MODEL_DIR}/tokenizer.json" ]; then
1317
echo "PIGuard model already present in ${MODEL_DIR}; nothing to do."
1418
exit 0
1519
fi
1620

17-
PIGUARD_REF="${PIGUARD_REF:-v1.0.0}"
18-
19-
echo "Downloading and exporting PIGuard model (${PIGUARD_REF})..."
20-
echo "Output directory: ${MODEL_DIR}"
21+
echo "Downloading and exporting PIGuard model into ${MODEL_DIR}..."
2122

2223
# Run the export in a disposable Python container so no host Python/toolchain is
23-
# required. The guard repo's export_onnx.py downloads the model from HuggingFace
24-
# and converts it to ONNX.
24+
# required. The export script downloads the model from HuggingFace and converts
25+
# it to ONNX.
2526
#
2627
# DEBIAN_FRONTEND=noninteractive keeps apt-get quiet on headless hosts.
27-
# GIT_CONFIG_GLOBAL turns off the detached-HEAD advice and clone progress spam.
28-
# HF_TRUST_REMOTE_CODE=1 allows the PIGuard model's custom modeling code to run
29-
# without an interactive prompt. Pass through HF_TOKEN if set on the host.
28+
# HF_TRUST_REMOTE_CODE=1 is belt-and-suspenders. Pass HF_TOKEN through if set
29+
# on the host for higher HF rate limits.
3030
DOCKER_ENVS=(
3131
-e HF_HOME=/tmp
3232
-e DEBIAN_FRONTEND=noninteractive
33-
-e GIT_CONFIG_GLOBAL=/tmp/.gitconfig
3433
-e HF_TRUST_REMOTE_CODE=1
3534
)
3635
if [ -n "${HF_TOKEN:-}" ]; then
3736
DOCKER_ENVS+=(-e "HF_TOKEN=${HF_TOKEN}")
3837
fi
3938

39+
# Pinned versions from go-prompt-injection-guard/scripts/requirements.txt.
40+
PYTHON_REQS="torch==2.12.0 transformers==5.10.2 onnxscript==0.7.0"
41+
4042
docker run --rm \
4143
-v "${MODEL_DIR}:/out" \
44+
-v "${EXPORT_SCRIPT}:/src/export_onnx.py:ro" \
4245
"${DOCKER_ENVS[@]}" \
4346
python:3.12-slim bash -c "
4447
set -euo pipefail
4548
46-
# Silence git advice / progress noise inside the container.
47-
printf '[advice]\ndetachedHead = false\n' > /tmp/.gitconfig
48-
49-
echo '==> Installing git and curl inside container...'
50-
apt-get -qq update
51-
apt-get -qq install -y --no-install-recommends git curl ca-certificates >/dev/null
52-
53-
echo '==> Cloning go-prompt-injection-guard (${PIGUARD_REF})...'
54-
git clone --quiet --depth 1 --branch '${PIGUARD_REF}' \
55-
https://github.com/BackendStack21/go-prompt-injection-guard.git /src
56-
57-
cd /src
58-
59-
# The PIGuard model uses custom HF modeling code. The upstream export script
60-
# does not pass trust_remote_code=True, so we patch it to avoid the interactive
61-
# y/N prompt that would hang in a non-TTY container.
62-
echo '==> Patching export script to allow custom HF modeling code...'
63-
sed -i 's/revision=MODEL_REVISION)/revision=MODEL_REVISION, trust_remote_code=True)/g' scripts/export_onnx.py
64-
65-
echo '==> Installing Python export requirements (this may take a minute)...'
66-
pip install --root-user-action=ignore --disable-pip-version-check --no-cache-dir --quiet -r scripts/requirements.txt
49+
echo '==> Installing Python export requirements (~2 GB, this may take several minutes)...'
50+
pip install --root-user-action=ignore --disable-pip-version-check --no-cache-dir --quiet ${PYTHON_REQS}
6751
6852
echo '==> Exporting PIGuard model from HuggingFace to ONNX (~735 MB, be patient)...'
69-
python -u scripts/export_onnx.py
53+
python -u /src/export_onnx.py
7054
7155
echo '==> Copying exported model to host volume...'
7256
cp ~/.cache/piguard/onnx/* /out/

docker/piguard/export_onnx.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python3
2+
"""Export the PIGuard model from HuggingFace to ONNX.
3+
4+
Derived from the go-prompt-injection-guard export script, with
5+
`trust_remote_code=True` added so it runs non-interactively in a container.
6+
See: https://github.com/BackendStack21/go-prompt-injection-guard
7+
"""
8+
9+
from pathlib import Path
10+
import torch
11+
from transformers import AutoModelForSequenceClassification, AutoTokenizer
12+
13+
MODEL_ID = "leolee99/PIGuard"
14+
# Pinned to a specific commit for supply-chain safety.
15+
MODEL_REVISION = "dd78b24e330193a22d2293ac66922dd4f982f563"
16+
OUTPUT_DIR = Path.home() / ".cache" / "piguard" / "onnx"
17+
18+
19+
def main():
20+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
21+
onnx_path = OUTPUT_DIR / "model.onnx"
22+
23+
if onnx_path.exists():
24+
print(f"ONNX model already exists at {onnx_path}")
25+
print("Delete it to re-export.")
26+
return
27+
28+
print(f"Downloading {MODEL_ID}@{MODEL_REVISION} from HuggingFace...")
29+
tokenizer = AutoTokenizer.from_pretrained(
30+
MODEL_ID, revision=MODEL_REVISION, trust_remote_code=True
31+
)
32+
model = AutoModelForSequenceClassification.from_pretrained(
33+
MODEL_ID, revision=MODEL_REVISION, trust_remote_code=True
34+
)
35+
model.eval()
36+
37+
# Save tokenizer (tokenizer.json is used by Rust)
38+
tokenizer.save_pretrained(str(OUTPUT_DIR))
39+
40+
# Save config.json for label mapping
41+
model.config.save_pretrained(str(OUTPUT_DIR))
42+
43+
# Dummy input for export
44+
dummy = tokenizer("Hello world", return_tensors="pt", truncation=True, max_length=512)
45+
46+
print("Exporting to ONNX...")
47+
torch.onnx.export(
48+
model,
49+
(),
50+
str(onnx_path),
51+
input_names=["input_ids", "attention_mask"],
52+
output_names=["logits"],
53+
dynamic_axes={
54+
"input_ids": {0: "batch", 1: "seq"},
55+
"attention_mask": {0: "batch", 1: "seq"},
56+
"logits": {0: "batch"},
57+
},
58+
opset_version=18,
59+
do_constant_folding=True,
60+
kwargs={
61+
"input_ids": dummy["input_ids"],
62+
"attention_mask": dummy["attention_mask"],
63+
},
64+
)
65+
66+
onnx_size = sum(
67+
f.stat().st_size for f in OUTPUT_DIR.iterdir()
68+
if f.suffix in (".onnx", ".data") and "model" in f.name
69+
)
70+
print(f"Done! Model saved to {OUTPUT_DIR}")
71+
print(f"Total size: {onnx_size / 1024 / 1024:.0f} MB")
72+
73+
74+
if __name__ == "__main__":
75+
main()

0 commit comments

Comments
 (0)