feat(pii): add opt-in GLiNER NER engine (PII_ENGINE), device-agnostic#5495
Conversation
Swap the 4 NER entity types (PERSON/LOCATION/NRP/DATE_TIME) to a single multilingual GLiNER zero-shot model when PII_ENGINE=gliner; spaCy stays the default and all ~36 regex/checksum recognizers are identical on both engines. Device-agnostic via PII_DEVICE / cuda auto-detect — same code on Fargate CPU now and EC2-GPU later. - engines.py: side-effect-free builders; SharedModelGLiNERRecognizer loads ONE model shared across the 5 per-language instances and restricts labels to the entities it owns; small spaCy models keep tokenization/lemmas for the regex recognizers; fail-fast on the lean image - pii.Dockerfile: multi-stage — default target unchanged (lean spaCy); --target gliner is a superset (torch CPU + gliner + baked model) where both engines work; gliner-gpu scaffold for the GPU fleet - CI publishes the gliner variant (:staging-gliner/:latest-gliner, amd64) - Helm: pii.engine / pii.device values wired to PII_ENGINE/PII_DEVICE - scripts/bench_engines.py: throughput + NER-parity diff harness - tests: unit (mocked GLiNER) + in-image integration for both engines Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Up3F97mjCH9HCj1pX4J8VJ
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
PR SummaryMedium Risk Overview The gliner engine swaps spaCy NER for one shared multilingual GLiNER model ( Helm exposes Reviewed by Cursor Bugbot for commit bff7626. Bugbot is set up for automated code reviews on this repo. Configure here. |
Greptile SummaryThis PR adds GLiNER as an opt-in zero-shot NER engine alongside spaCy, selected via
Confidence Score: 5/5Safe to merge — the spaCy default path is construction-identical to the code it replaces, and the GLiNER path is gated behind an explicit env var that is unset in all current deployments. The refactor correctly extracts engine builders into a dedicated module without changing the spaCy path's observable behavior. The shared-model cache, label-scoping override, and fail-fast guards are all correctly implemented and tested. No logic regressions were found in the dispatch flow, the VIN check-digit validator, or the Helm chart rendering. No files require special attention. The extraEnvVars bypass noted in a previous review thread is a pre-existing Helm pattern limitation, not introduced by this PR. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[container start] --> B{PII_ENGINE?}
B -- "invalid" --> C[ValueError at import\nfail fast]
B -- "spacy (default)" --> D[build_spacy_analyzer\n5x spaCy lg models]
B -- "gliner" --> E{deps present?}
E -- "gliner pkg missing" --> F[RuntimeError\nfail fast]
E -- "sm models missing" --> G[RuntimeError\nfail fast]
E -- "ok" --> H[build_gliner_analyzer\n5x sm models for tokenization]
H --> I[remove SpacyRecognizer\nfrom registry]
I --> J{assert removal}
J -- "failed" --> K[RuntimeError\nPresidio layout changed]
J -- "ok" --> L[add 5x SharedModelGLiNERRecognizer\nshared _shared_models cache]
D --> M[_register_common_recognizers\nVIN x5 languages + 21 EXTRA_RECOGNIZERS]
L --> M
M --> N[AnalyzerEngine ready]
N --> O[FastAPI app serves /analyze /redact /redact_batch]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[container start] --> B{PII_ENGINE?}
B -- "invalid" --> C[ValueError at import\nfail fast]
B -- "spacy (default)" --> D[build_spacy_analyzer\n5x spaCy lg models]
B -- "gliner" --> E{deps present?}
E -- "gliner pkg missing" --> F[RuntimeError\nfail fast]
E -- "sm models missing" --> G[RuntimeError\nfail fast]
E -- "ok" --> H[build_gliner_analyzer\n5x sm models for tokenization]
H --> I[remove SpacyRecognizer\nfrom registry]
I --> J{assert removal}
J -- "failed" --> K[RuntimeError\nPresidio layout changed]
J -- "ok" --> L[add 5x SharedModelGLiNERRecognizer\nshared _shared_models cache]
D --> M[_register_common_recognizers\nVIN x5 languages + 21 EXTRA_RECOGNIZERS]
L --> M
M --> N[AnalyzerEngine ready]
N --> O[FastAPI app serves /analyze /redact /redact_batch]
Reviews (2): Last reviewed commit: "refactor(pii): ship both engines in one ..." | Re-trigger Greptile |
…flip Collapse the gliner build target into the single pii image: spaCy lg models, torch (CPU), gliner, and the baked GLiNER weights all ship in it, so PII_ENGINE switches engines with no image swap and no tag matrix. CI reverts to the single pii build (no -gliner tags). The GPU variant becomes the same Dockerfile built with --build-arg TORCH_INDEX_URL=.../cu128. Image grows ~6.1GB -> ~9.6GB. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Up3F97mjCH9HCj1pX4J8VJ
|
@greptile review |
What
Adds GLiNER (zero-shot transformer NER) as an opt-in engine for the PII service, selected by
PII_ENGINE(spacy|gliner, defaultspacy). Of the ~40 supported entity types only 4 are NER —PERSON,LOCATION,NRP,DATE_TIME— and GLiNER replaces exactly those; all ~36 regex/checksum recognizers (locale IDs, credit card, email, VIN, …) are registered identically on both engines.One image, one tag. The pii image ships both engines (spaCy
*_lgmodels + torch-CPU + gliner + baked GLiNER weights), so switching engines is a pure env flip — no image swap, no tag matrix, and rollback isPII_ENGINE=spacy. Image grows ~6.1GB → ~9.6GB.Device-agnostic:
PII_DEVICE(cpu/cuda) or auto-detect. The same code runs on Fargate CPU today and the EC2-GPU fleet later — no code change to switch. GPU capacity/scheduling is a separate infra task (not touched here).Design
apps/pii/engines.py(new): side-effect-free engine builders extracted fromserver.py(which is now just env dispatch + endpoints; the spacy path is construction-identical to before).urchade/gliner_multi_pii-v1, override viaPII_GLINER_MODEL) shared across the 5 per-language recognizer instances —SharedModelGLiNERRecognizer.load()caches the ~1.2GB model instead of loading it 5×.analyze()restricts requested entities to the 4 it owns: presidio passes the full ~40-entity list to every recognizer and stockGLiNERRecognizerwould append unknown entities (CREDIT_CARD,VIN,ES_NIF, …) as ad-hoc zero-shot labels — wrong scope and slower.*_sm, ~60MB total) purely for tokenization + lemmas soNlpArtifacts/context-boosts for the regex recognizers behave as before (blank pipelines don't work — presidio tries to pip-download them).SpacyRecognizeris removed from the registry so sm-model NER never competes with GLiNER.PII_ENGINEdies at import; a custom image missing gliner deps dies with an actionable message instead of presidio's silent pip-download fallback.docker/pii.Dockerfile: single linear image. GLiNER weights are baked at build (cached layer) withHF_HUB_OFFLINE=1so startup never touches the network and a typo'dPII_GLINER_MODELfails loudly. The GPU variant for the infra follow-up is the same Dockerfile built with--build-arg TORCH_INDEX_URL=https://download.pytorch.org/whl/cu128(torch pinned to the newest version published on both cpu and cu128 indexes). Bench + tests + pytest ride along in the image.pii.engine(spacy|gliner, defaultspacy) andpii.devicerender toPII_ENGINE/PII_DEVICEas chart-owned env keys (pii.envcan't override them;PII_GLINER_MODELstays a plainpii.envpassthrough).nvidia.com/gpurequests are deliberately not wired — infra follow-up.PII_WORKERS(merged from feat(pii): env-driven uvicorn worker count (PII_WORKERS) #5457) note: each uvicorn worker loads its own GLiNER model copy — into GPU memory on cuda — so GPU deployments generally wantPII_WORKERS=1per GPU (documented in Dockerfile + server.py; behavior unchanged).Parity & throughput (spaCy vs GLiNER-CPU)
apps/pii/scripts/bench_engines.pyruns the same payload through both engines and diffs results; the 32-text multilingual payload (en/es/it/pl/fi with names, locations, dates, NRP, and regex bait: emails, phones, valid-checksum VIN/ES_NIF/IT_FISCAL_CODE/PESEL/HETU) ships in the repo. Numbers below from the CPU (aarch64 docker) run; rerun withdocker run --rm <pii-image> python scripts/bench_engines.py:*_lg)CPU-GLiNER is ~80× slower than spaCy — that's the point of this being opt-in now and GPU-bound later.
NER agreement (span IoU ≥ 0.5, same type):
Regex/checksum entities: identical across engines ✓ (the script exits non-zero on any non-NER diff).
Qualitative diff highlights: GLiNER has higher recall overall and produces wider (often better) spans (
45 Queen Street, Torontovs justToronto), but zero-shot eagerness tags pronouns (She,he) and once an email as PERSON; spaCy has its own false positives (quarterlyas DATE_TIME, credit-card number as ORGANIZATION) and misses GLiNER catches.GLINER_ENTITY_MAPPINGlabel prompts are the tuning lever; the bench is the harness.Enabling
PII_ENGINE=glineron the pii task def (same image). Realistically waits for the GPU capacity provider given CPU throughput.pii.engine: gliner— nothing else.docker run -e PII_ENGINE=gliner ....Validation
PII_ENGINEunset ⇒ spaCy, regression-tested/redact_batchen+es byte-identical to today;PII_ENGINE=bogusdies at import with a clear error.PII_ENGINE=glinermasks PERSON/LOCATION/email/phone/VIN/ES_NIF; flipping back tospacyon the same image behaves identically to default; startup is fully offline.apps/pii/tests— unit suite (mocked GLiNER; shared-model load-once, label restriction, fail-fast) runs model-free; integration suite (RUN_PII_INTEGRATION=1) ran green in-image for both engines.helm unittest65/65 (incl. new engine/device/override tests),helm lintclean,bun run lint:checkgreen, ruff clean.Pre-existing quirk noted while validating (not addressed here):
/analyzewithreturn_decision_process=true500s on both engines (presidio'sAnalysisExplanationisn't JSON-serializable viato_dict()); the app never sends that flag.Follow-ups (infra task)
nvidia.com/gpurequests; build/publish the cu128 image viaTORCH_INDEX_URLbuild-arg; setPII_WORKERS=1per GPU.🤖 Generated with Claude Code
https://claude.ai/code/session_01Up3F97mjCH9HCj1pX4J8VJ