GROBID is only needed during reference extraction (ra_grobid_backfill.py), not for normal corpus operations (search, embeddings, wiki). Idle it still holds ~3.5GB on the 8GB Jetson, so keep it stopped by default. - infra/docker-compose.yml: grobid restart unless-stopped -> "no" (no auto-start on boot; db keeps unless-stopped). Start/stop papers-grobid around an R-A run. - docs/infra/grobid-jetson.md: add "Run on-demand" section with the start/stop workflow, and record the refextract head-to-head evaluation on the Jetson that led to keeping GROBID: far worse DOI recall on this math corpus (lutz thesis 101 usable IDs vs refextract 2) and slower per PDF despite a smaller footprint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
193 lines
7.9 KiB
Markdown
193 lines
7.9 KiB
Markdown
# GROBID on the Jetson (aarch64) — deploy runbook
|
||
|
||
**Status:** ACTIVE
|
||
**Host:** `alfred@192.168.178.103` — NVIDIA Jetson Orin Nano (aarch64, 8 GB RAM, 8 GB swap, 6 cores)
|
||
**Service:** GROBID reference extraction for roadmap **R-A** (`scripts/ra_grobid_backfill.py`)
|
||
**Last updated:** 2026-06-17
|
||
**Verified:** 2026-06-17 — `grobid/grobid:0.9.0-crf` pulled as native `arm64`, `/api/isalive`→`true`,
|
||
end-to-end `extract_references` on `lutz-2024-thesis.pdf` → 116 refs (101 with DOI/arXiv) in ~36s.
|
||
|
||
---
|
||
|
||
## Why this image (`grobid/grobid:0.9.0-crf`)
|
||
|
||
GROBID had never come up on the Jetson because `infra/docker-compose.yml` pinned
|
||
`grobid/grobid:0.8.2`, and **every `0.8.x` tag (and the full `0.9.0`) is published
|
||
amd64-only**. A Docker tag resolves to a multi-arch *manifest list*; on aarch64
|
||
the `0.8.2` list has no `linux/arm64` entry, so there is simply nothing to pull or
|
||
run (hence `curl localhost:8070` → connection refused, nothing listening).
|
||
|
||
The fix is to select a tag whose manifest list actually contains a `linux/arm64`
|
||
build:
|
||
|
||
| Candidate | arm64? | Size | Notes |
|
||
|-------------------------------|:------:|--------:|----------------------------------------|
|
||
| `grobid/grobid:0.8.2` | ✗ | ~9.5 GB | full DL image, amd64-only (old pin) |
|
||
| `grobid/grobid:0.9.0` / `-full`| ✗ | ~10 GB | full DL image, amd64-only |
|
||
| **`grobid/grobid:0.9.0-crf`** | **✓** | ~500 MB | **CRF-only, official, multi-arch** ← |
|
||
| `lfoppiano/grobid:0.9.0-crf` | ✓ | ~500 MB | same build, community namespace (alt.) |
|
||
|
||
We use **`grobid/grobid:0.9.0-crf`**:
|
||
|
||
- **CRF-only** is sufficient for reference extraction (`/api/processReferences`)
|
||
and is ~500 MB vs ~10 GB for the deep-learning image.
|
||
- **Native arm64** — no qemu emulation (too slow / RAM-heavy on a Jetson).
|
||
- **Official namespace**, **pinned version** (not `latest-crf`, which is a moving
|
||
target). arm64 builds only exist from `0.9.0-crf` onward — there is no arm64
|
||
`0.8.x`, so this is a forced (but compatible) bump from the old `0.8.2` pin. The
|
||
TEI that `codex/parsing/grobid.py` parses (`biblStruct`, `persName`,
|
||
`idno[@type='DOI']`, …) is unchanged across the bump.
|
||
|
||
> Caveat from upstream: the arm64 image is documented as "tested only on macOS,
|
||
> not linux/arm64". The container is the *same* `linux/arm64` ELF either way, so
|
||
> bare-metal Jetson works; the end-to-end check below is what de-risks it.
|
||
|
||
---
|
||
|
||
## One-time prerequisites
|
||
|
||
### 1. Docker permission for `alfred`
|
||
|
||
The SSH user `alfred` is in the `sudo` group but **not** the `docker` group, so
|
||
`docker …` fails with `permission denied … /var/run/docker.sock`. Grant access
|
||
once (requires the sudo password):
|
||
|
||
```bash
|
||
ssh alfred@192.168.178.103
|
||
sudo usermod -aG docker alfred # add to docker group (persistent)
|
||
```
|
||
|
||
Then **start a fresh login** so the new group takes effect (group membership is
|
||
only re-evaluated at login — an existing session/`ssh` won't see it; a *new*
|
||
`ssh` connection will):
|
||
|
||
```bash
|
||
exit && ssh alfred@192.168.178.103
|
||
docker ps # should now work without sudo
|
||
```
|
||
|
||
(Alternatively run all `docker` commands below under `sudo`, but group membership
|
||
is cleaner and lets `ra_grobid_backfill.py` / curl checks be driven over plain SSH.)
|
||
|
||
### 2. Docker Compose v2 plugin (arm64)
|
||
|
||
This host has Docker Engine but **not** the Compose v2 CLI plugin (on Linux they
|
||
are separate packages; only Docker Desktop bundles Compose). Without it
|
||
`docker compose …` fails with `docker: unknown command: docker compose`. Install
|
||
the arm64 plugin into the user-local plugin dir (no sudo needed):
|
||
|
||
```bash
|
||
mkdir -p ~/.docker/cli-plugins
|
||
curl -sSL https://github.com/docker/compose/releases/latest/download/docker-compose-linux-aarch64 \
|
||
-o ~/.docker/cli-plugins/docker-compose
|
||
chmod +x ~/.docker/cli-plugins/docker-compose
|
||
docker compose version # confirm it resolves
|
||
```
|
||
|
||
---
|
||
|
||
## Deploy
|
||
|
||
GROBID is **service-scoped** on purpose: Postgres (`papers-db`) is already running
|
||
on this host, so we bring up only the `grobid` service and never `docker compose up`
|
||
the whole stack (that would try to start a second `papers-db`).
|
||
|
||
```bash
|
||
# copy this repo's compose file to the Jetson (first time only)
|
||
scp infra/docker-compose.yml infra/schema.sql alfred@192.168.178.103:~/codex-infra/
|
||
|
||
ssh alfred@192.168.178.103
|
||
cd ~/codex-infra
|
||
docker compose up -d grobid # pulls grobid/grobid:0.9.0-crf (arm64), starts papers-grobid
|
||
docker compose logs -f grobid # wait for "Started ... in N seconds" (model load ~20–40s)
|
||
```
|
||
|
||
The `deploy.resources.limits.memory: 4g` cap in the compose file is honored by
|
||
Compose v2 here (no swarm needed). 3 GB suffices for references; 4 GB leaves
|
||
headroom alongside Postgres. The `restart: "no"` policy means GROBID does **not**
|
||
auto-start on boot — see "Run on-demand" below.
|
||
|
||
---
|
||
|
||
## Run on-demand (stop when idle)
|
||
|
||
GROBID is only needed **during reference extraction** (`scripts/ra_grobid_backfill.py`);
|
||
nothing else in the stack uses it. Idle it still holds ~3.5 GB, so on the 8 GB Jetson
|
||
keep it stopped and start it only for an R-A run:
|
||
|
||
```bash
|
||
ssh alfred@192.168.178.103 'docker start papers-grobid' # ~30s model load
|
||
curl -s --retry 30 --retry-delay 2 --retry-connrefused \
|
||
http://192.168.178.103:8070/api/isalive # wait for: true
|
||
|
||
# ... run R-A from the Mac ...
|
||
PYTHONPATH=. python scripts/ra_grobid_backfill.py --write
|
||
|
||
ssh alfred@192.168.178.103 'docker stop papers-grobid' # reclaim ~3.5 GB
|
||
```
|
||
|
||
First-time deploy uses `docker compose up -d grobid` (above); afterwards the
|
||
container persists, so `docker start/stop papers-grobid` is all you need.
|
||
|
||
> Why on-demand rather than a lighter tool: refextract was evaluated head-to-head
|
||
> on the Jetson and rejected — far worse DOI recall on this math corpus (lutz
|
||
> thesis: GROBID **101** usable IDs vs refextract **2**), and it was *slower* per
|
||
> PDF despite a smaller footprint. GROBID stays; running it on-demand removes its
|
||
> only real downside (idle RAM).
|
||
|
||
---
|
||
|
||
## Verify `GET /api/isalive` → `true`
|
||
|
||
```bash
|
||
# on the Jetson
|
||
curl -s http://localhost:8070/api/isalive # => true
|
||
|
||
# from the Mac (LAN) — same value the corpus' GROBID_URL points at
|
||
curl -s http://192.168.178.103:8070/api/isalive # => true
|
||
```
|
||
|
||
Anything other than `true` (timeout / connection refused / `false`) means the
|
||
container isn't up — check `docker compose logs grobid` and that nothing else
|
||
holds port 8070.
|
||
|
||
---
|
||
|
||
## End-to-end reference-extraction check + running R-A
|
||
|
||
With `GROBID_URL=http://192.168.178.103:8070` (already set in `.env.jetson-ingest`):
|
||
|
||
```bash
|
||
# one PDF, references only — should print N>0 parsed references
|
||
PYTHONPATH=. python - <<'PY'
|
||
from codex.parsing.grobid import extract_references
|
||
refs = extract_references("/Users/tarikmoussa/Desktop/ConformalLabpp/papers/lutz-2024-thesis.pdf",
|
||
grobid_url="http://192.168.178.103:8070")
|
||
print(f"{len(refs)} references; first DOI/arXiv:",
|
||
next(((r['doi'], r['arxiv_id']) for r in refs if r['doi'] or r['arxiv_id']), None))
|
||
PY
|
||
```
|
||
|
||
Then run the references-only backfill (DB tunnel up — see
|
||
`docs/audit/DATA-QUALITY-2026-06-15.md`):
|
||
|
||
```bash
|
||
ssh -N -L 5433:localhost:5432 alfred@192.168.178.103 & # DB tunnel
|
||
cp .env.jetson-ingest .env
|
||
PYTHONPATH=. python scripts/ra_grobid_backfill.py --zero-edge-only # dry-run first
|
||
PYTHONPATH=. python scripts/ra_grobid_backfill.py --write # apply
|
||
```
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
- **Container killed mid-request / exit 137** — OOM. The CRF image needs ~3 GB for
|
||
references; raise the compose `memory` limit or check Postgres isn't starving the
|
||
box (`free -h`, `docker stats papers-grobid`).
|
||
- **`isalive` true but `processReferences` 500s** — usually a `pdfalto` failure on a
|
||
malformed PDF; confirm with a clean PDF (`lutz-2024-thesis.pdf`) and check
|
||
`docker compose logs grobid`.
|
||
- **`permission denied … docker.sock`** — the docker-group prerequisite above wasn't
|
||
applied, or the SSH session predates it (re-login).
|