pip install jevimageinstalls no torch.jevimage.connect(url)works fully;jevimage.load()does not. See Install.- A trained head belongs to one encoder. It records which one and refuses to run under another.
Contents
- Install and import
- Loading an encoder
- The first run takes minutes
- Questions the engine refuses
noulspecifically- Temperature
- Trained heads
- Training refuses your data
- Out of memory while training
nanin an answer- Reading images
- Talking to a server
- Reading an
Answer - HTTP status codes at a glance
Install and import
jevimage.load runs the encoder in this process, which needs torch and transformers
pip install jevimage is deliberately a few hundred kilobytes: pillow and the
standard library. jevimage/__init__.py resolves load, Jev, Encoder, register,
available and DEFAULT through a module-level __getattr__, so importing the package
never imports torch. Touching load is the moment the heavy half is needed, and that is
where the ImportError is raised, with both fixes named.
Fix. Pick the one that matches your situation.
Remote has the same method names, takes the same arguments and returns the same
Answer objects, including train(), which uploads the images and fits the head on the
server. The only missing method is ask_embedding, because its whole purpose is to skip
the network. One argument is local-only: Jev.train and Jev.compare take template=
to set the zero-shot baseline prompt ("a photo of {}" by default), and the wire format
carries no equivalent.
Confirming an API-only install is healthy, in a venv with no torch at all:
available() works without torch on purpose: seeing which encoders exist should not
require the stack that runs them.
jev: this command runs the encoder locally, which needs torch
ImportError, checks for torch/transformers in the message
and prints both fixes, rather than letting No module named 'torch' escape; that
message does not tell you which of the two fixes you want. Exit status is 1, with no
traceback.
Fix. Either install the extra, or add --url / $JEV_URL:
ask, train, heads, rm. jev encoders needs
nothing and always works. jev serve is local.
jev heads with no --url also needs torch. Not because of the files: the local
branch of cmd_heads imports jevimage.training for its one formatting helper, and
that module imports torch at module scope. So it fails with the message above even when
the head directory is empty or absent. jev heads --url ... never takes that branch, and
is the path that exists for API-only installs:
jev serve needs uvicorn
pip install 'jevimage[serve]' (fastapi + uvicorn). See
Serving.
No module named 'open_clip'
Only dfn5b-h-14-384 needs it. pip install 'jevimage[openclip]'. Those are Apple’s
DFN5B weights under Apple’s own licence; read it before you redistribute anything built
on them. Every other registry entry runs on transformers alone.
Loading an encoder
unknown encoder 'siglip-base'
jevimage.available() or jev encoders. If it is your own
encoder, note the second half of that message: register() mutates a dict in your
process. The jev command never imports your code, so a name you registered in a script
is invisible to the CLI. To serve a custom encoder, build the app yourself:
Registry and ensemble errors
Every empty-member spelling is refused, with the same sentence, before any member is
resolved, so a member name in the string is never even looked up:
+ is not a shorthand for a single one: a++b is refused, not
read as a+b. An encoder name is provenance (a head records the exact string), so a typo
that would have loaded under a different name than the one you wrote is refused rather
than accepted.
(an ensemble needs at least 2 members still exists, but only for
jevimage.encoders.Ensemble([one]) built by hand; load() never reaches it.)
An ensemble’s members each load fully, so a typo in the second member is only reported
after the first one has downloaded. Check names with jev encoders first.
Custom encoders: 'Half' object has no attribute 'img'
jevimage.Encoder and forgetting the same method gives the base class’s
stub instead:
load() accepts any object as-is when it is not a string, which is how a custom
encoder gets in without registering it, and the contract is checked by use, not by an
isinstance gate.
Fix. An encoder needs exactly four things:
logit_scale and logit_bias are optional; see noul specifically.
embeddings must be L2-normalised
1.0 and 0.0, the one output a
probability-first API must never produce by accident. The check tolerates 1e-2, so
ordinary float error passes.
Fix. jevimage.encoders.unit() does the job on anything a model returns: a tensor, a
tuple, or a transformers output object with image_embeds / text_embeds /
pooler_output / last_hidden_state. It also mean-pools a 3-D token-level output and
casts to fp32.
norm 0, means an all-zero embedding, usually an encoder
that silently failed rather than one that forgot to normalise.
The first run takes minutes
jevimage.load() with no arguments loads siglip2-base-224. Nothing is bundled with the
package; weights come from the Hugging Face hub on first use and are cached by
huggingface_hub afterwards. The first call downloads hundreds of megabytes (several
gigabytes for siglip2-giant-384) with no jevimage-specific progress output, so a
process that looks hung on load() is usually downloading.
Loading siglip2-base-224 from an already-populated cache, measured here:
- Warm the cache before you need it (a Dockerfile layer, a CI step, an init container) so the first user request is not the first download.
HF_HOME/HF_HUB_CACHEcontrol where the cache lives. Point them at a volume that survives container restarts, or you re-download on every deploy.HF_HUB_OFFLINE=1makes a missing cache fail immediately instead of hanging on a slow network. Useful for proving the cache is warm.- Set
JEV_ENCODERso every process agrees which weights to warm. - Instances are cached per name inside the process, so calling
load("siglip2-base-224")twice loads the weights once. - A server pays this once at startup. That is the strongest argument for
connect()overload()in short-lived processes: a script that runs for two seconds should not spend another five to twenty-five loading a model.
jevimage.connect(url) downloads nothing, ever.
Questions the engine refuses
A malformed question is not free:ask() encodes the image first and plans the questions
afterwards, so a bad question in a batch of 64 fails the whole call after paying for the
encode. (The CLI’s own flag parsing is the exception, which happens before any weights
load.) Types and shapes are in Question types; this is the failure list.
Note the asymmetry between
choice and score: choice takes a dict ({label: caption}) or a bare list, score takes an ordered list only. That is the single
most common shape mix-up. A dict passed to score gets the “ordered criteria array”
message.
A choice with 255 options is legal and cheap, because the captions are encoded once and
cached per string, so two questions sharing an option pay for it once.
noul specifically
noul is the yes/no probability, and it has two forms. The difference is the source of
almost every noul error.
encoder 'toy' has no calibrated sigmoid
sigmoid(cos * scale + bias). Those two constants are learned during pairwise training; without them there is no
honest mapping from a cosine to a probability, and inventing one would return a number
that looks like a probability and is not. So it raises instead.
Who has them.
Measured on the real default encoder:
Ensemble.logit_scale is exactly that whenever
every member publishes one. The default temperature is derived from it. The bias does not
average: it was fitted against each member’s own negative set, and there is no pair to
calibrate across a concatenation. So it stays None, and the single-caption form stays
refused:
logit_scale None, because a mean of
nothing is nothing:
noul against a server you do not run, check
connect(url).health() for the encoder field; there is no encoder object on the
client side to inspect.
noul criteria has an empty 'false' caption
criteria entirely and use instructions.
noul needs instructions, or a {"true": ..., "false": ...} criteria pair
{"type": "noul"} with neither. Supply one of the two.
Temperature
- 0 divides every logit by zero:
nanorinf, no usable answer. - negative inverts every softmax. The least similar option wins and the answer still reports high confidence. This is the dangerous one: it produces a perfectly well-formed, confidently wrong answer that nothing downstream can detect.
- inf divides every logit to zero, so every option comes back equally likely with confidence 0. A well-formed answer carrying no signal, which is worse than an error because it looks fine.
- a string is rejected rather than coerced;
"0.1"from an unparsed config file should say so. Trueis anintin Python and would silently meantemperature=1. Booleans are excluded explicitly.
1 / logit_scale when the encoder publishes one (so 1/112 ≈ 0.0089 for
siglip2-base-224), and 0.01 otherwise, the CLIP-family convention. A raw cosine
lives in roughly [0, 0.3] and softmaxes to nearly uniform, which is why the scale is
needed at all.
Remote.ask runs its own version of this check before sending, so 0, a negative, a
string and True are all local ValueErrors and never become a round trip, though its
wording is the must be a number greater than 0 one in every case. It does not test for
inf, which travels and comes back as the engine’s own message in a 422.
Trained heads
A head is a small matrix that lives in one encoder’s embedding space. Most head errors are that identity being enforced.unknown head 'nope'; trained heads: colours
none trained yet when the directory is empty). Check
with jev heads, and check you are pointed at the right directory: heads_dir=, else
$JEV_HEADS_DIR, else $JEV_HOME/heads, else ~/.jev/heads.
A head file that exists but fails to load gets its own message, not this one. See
a head file that will not load.
head 'colours' was trained on encoder 'toy' but this instance runs 'toy-calibrated'
compare() enforces the same rule, for the same reason:
Toy is a class you hold, so it is loaded as an instance. A registry name - jevimage.load("siglip2-base-224") - is the other spelling; jevimage.load("toy") is neither, and raises unknown encoder 'toy'.)
This is also why changing $JEV_ENCODER on a server invalidates every head it holds.
Retrain them, or run a second server.
head 'colours' expects 16-dim embeddings, this encoder produces 32
The names matched but the widths did not. Two different builds were sharing one name.
Names are identities here; give the new one its own name and retrain.
head 'nopro' records no encoder, so it cannot be checked
head 'damaged' lists 3 classes but its weights have 2 columns
invalid head name '../evil'
ask() that follows cannot find what train() said it
saved. The client validates the name before it builds a URL from it, so this is a local
ValueError even against a server, and you never see a confusing 404 for it.
A head file that will not load
Drop a file that is not a valid head into the heads directory and the store records it as broken rather than crashing:jev heads surfaces it on stderr, so it does not vanish from the listing:
ask(), gives the same accurate message. The
store counts a file it could not read as present, so neither route claims the head is
unknown while it is sitting there on disk:
ValueError: unknown head ... therefore means what it says: no such file. Compare:
jev heads (stderr), jev heads --json (the broken key) and jev.heads.broken list
every such file at once. The usual causes:
- The file was written by a newer torch, or is not a torch file at all.
- A
.ptfrom somewhere else. Heads load withweights_only=True, so a pickle containing anything beyond tensors and plain metadata is refused, deliberately: a head file from a colleague cannot execute code on the way in. - A truncated copy.
save()writes to.pt.tmpandos.replaces it, so jevimage’s own writes are atomic; a truncated file came from something else copying it.
compare() returns None
Not an error. A head keeps the embeddings it was fitted on so the trained-vs-zero-shot
comparison stays answerable later. A head saved without them cannot be compared, and
compare() says None rather than inventing a baseline. The server says 409 with the
same reason, and Remote.compare() translates that back to None so both sides behave
identically:
Training refuses your data
The suffix list is short on purpose:
.jpg, .jpeg, .png, .webp. An animated GIF,
an SVG or a RAW file is not something to silently take the first frame of. Convert them
first if you need them.
“Training did not help.” That is a result, not an error. The CLI says so:
head.comparison["per_class"]; training often helps some classes and hurts others, and
the overall number hides that.
accuracy is None. k-fold uses k = min(5, thinnest class), and refuses to
report at k < 2, because a number computed on a fold that was missing a class is worse
than no number. The two-per-class floor above means train() never lands there, so a
None you see came from a head record that carries no cv_accuracy at all, one built or
edited by hand. jev heads prints - for it, not 0.0%.
Out of memory while training
There is noOutOfMemoryError with a helpful message here; you get CUDA’s or the OOM
killer’s.
Jev.train() embeds every image in one call to the encoder. Measured with an encoder
that counts its own batch sizes:
n × 3 × 224 × 224 × 4 bytes, about 600 KB per image, so 4000 images is ~2.4 GB of
pixels before the model has done anything. At 384px it is roughly three times that.
Fixes, cheapest first.
Jev.train, so remember head["encoder"]: without it the
head fails the provenance check with records no encoder.
Other levers: JEV_DEVICE=cpu trades speed for system RAM, which is usually far larger
than GPU RAM; a 224px encoder over a 384px one roughly thirds the pixel memory; and
training on a server (connect(url).train(...)) moves the problem to a machine sized for
it, though the images then cross the wire, bounded by the server’s own per-image and
per-request limits.
OOM at query time is a different thing: ask() embeds one image. If that OOMs, the
encoder is too large for the device, not the batch.
nan in an answer
You will not get a nan back. You get this instead:
json.dumps turns nan into invalid JSON
(literal NaN, which strict parsers reject), and a caller thresholding a probability
turns it into an unexplained crash three layers away. Refusing at the source, with the
field named, turns that into something you can act on.
What causes it. Not temperature, which is validated separately and cannot reach
here. In practice it is the encoder emitting nan:
- fp16 overflow. A model forced to fp16 on a GPU can produce
infin the forward pass, which becomesnanafter normalisation.unit()casts to fp32 and the framework’s own dtype choice is bf16 on CUDA and fp32 on CPU for exactly this reason: bf16 has too few mantissa bits for a stable softmax, and a probability that flips with the dtype is not a probability. - A custom encoder dividing by a zero norm.
unit()clamps at1e-12; hand-rolled normalisation often does not. - A degenerate image. A zero-byte or entirely blank input reaching a model that produces a zero vector.
got norm 0), because 0 compares cleanly against 1. A nan norm
compares False against every threshold, slips past that check, and is caught here.
Neither guard catches both cases, which is why both exist.
Reading images
to_image accepts: Path or str (an existing file), bytes (encoded image
bytes), a PIL.Image, or a data:image/...;base64,... string. Anything else is a
TypeError.
A JPEG, PNG or WebP travels to a server as the original bytes; anything else is
re-encoded to PNG first. Portrait phone photos are exif_transposed before encoding, on
both the local and the server path, so an image with Orientation=6 is not scored
sideways, and local and remote answers stay identical.
embed() needs at least one image means you passed [].
ask() takes one image, got a list of 2
ask() answers about one image. A list used to be encoded in full and then
answered for its first element only, paying the whole cost and discarding the rest
silently, so it is refused instead.
Both sides refuse the same way. Remote.ask raises the same TypeError, with the
same first sentence, before anything goes over the wire; it is not a server-side shape
error and not a different exception. The suggested fix differs, because ask_embedding
is local-only: over HTTP the sentence ends Loop over them, one ask() each.
Fix. Loop, or, locally where ask_embedding exists, encode once and read the rows:
Remote there is no ask_embedding, because that method’s whole purpose is to skip
the network, so loop over ask() there.
Talking to a server
JevError is the client’s exception. It subclasses ValueError deliberately, so an
existing except ValueError keeps working when a load() line becomes a connect()
line. It carries .status (the HTTP code) and .detail (the server’s own sentence). The
detail is the useful part:
url '127.0.0.1:8123' has no scheme
connect() before any request. urllib’s own message for this is unknown url type: 127.0.0.1, which points at the port and sends you looking in the wrong place.
Prefix the scheme.
Connection refused, or unknown host
.status is None: nothing answered, so there is no HTTP code - and .detail is
None too, for the same reason, so print the exception itself (print(e.status, e.detail or e)) if you want the sentence in that case. The OS reason in parentheses is
the diagnostic: Connection refused means wrong port or the process is
down; Name or service not known means DNS. Check curl http://host:port/v1/health;
health is open even on a keyed server precisely so that check always works.
did not answer within 60.0s
/v1/health to answer before
sending work:
train() upload, which sends every image in one
request. From the CLI that is --timeout SECONDS on jev ask and jev train, or
$JEV_TIMEOUT, which covers all four remote commands, with the same 60 s default:
401: send 'Authorization: Bearer <key>'
Authorization: secret without Bearer
is a 401, not a 400).
health()["auth"] says open or bearer,
and that endpoint needs no key, so you can always check which mode you are in. On the
server side, keys come from JEV_API_KEYS (comma- or space-separated) or jev serve --api-key K1 K2. A non-ASCII key is a plain 401, not a 500.
413: too large, four different ways
178956970 above is PIL’s ceiling, not the 50MP one), so adding
“the limit is 50MP per image” after it would name a limit that was not the one refused.
The third message is the 50MP guard, and it says so.
The image-count cap is not your problem for embed(): the client chunks at 8
automatically, so jev.embed([...20 images...]) works and matches Jev.embed exactly:
at most 8 images per request by building the HTTP request yourself.
Training is exempt: POST /v1/heads is a bulk upload bounded by MAX_EXAMPLES (4000),
not by 8, and it counts in its own noun:
MAX_IMAGES, MAX_B64 and MAX_PIXELS are
module-level constants in jevimage/server.py, so raising them means running a patched
server, not passing a flag.
415: unsupported format
convert(), because that is what drops .format. Convert first. An
unrecognisable format reports an unknown format.
422: malformed request
image and images together are refused, not resolved. They do not merely select
different pixels, they select different response shapes (image answers with one object
under answers, images answers with a list), so picking one silently would change what
the caller gets back. Send exactly one, on /v1/ask and on /v1/embed alike. A client
built on jevimage.connect() never sends both.
On POST /v1/heads the per-item prefix is examples[i], matching the field the item
came from:
ValueErrors also arrive as 422, with the message intact. That is the
design, not a leak. An unknown head over HTTP reads exactly as it does locally:
loc names the offending field. epochs is capped at 1..5000 because it is a loop count
a caller can set: 5000 epochs on 4000 examples is about 70 seconds on the 16-thread
laptop CPU these numbers were measured on, and 5 million is an outage.
404: head, or route
Jev (jev.head("nope")) names the directory it searched:
KeyError: "no head named 'nope' in /srv/jev/heads (have: ...)", because that is what
an operator at a REPL needs. The server strips any filesystem path out of a 404 body on
the way out, so a caller who can guess a head name cannot map the server’s disk. Nothing
to configure, and nothing to put behind a proxy for this.
A head file that exists but will not load is also a 404 here, carrying the store’s own
reason rather than “unknown head”:
Remote.head() propagates the 404; Remote.delete_head() translates it to False,
matching Jev.delete_head. (GET /v1/heads names no head, so it has no 404 of this
kind; an empty list is the answer.) The CLI turns that into an error with a non-zero
exit:
Not Found with no jevimage wording is a routing problem; check the path and
that you did not drop /v1.
409
Only from GET /v1/heads/{name}/compare, for a head saved without its embeddings. See
compare() returns None.
Reading an Answer
Answer is a dict subclass with attribute access, so it serialises to JSON unchanged.
Asking for a field the answer type does not have says which fields it does have:
choice and head carry choice / probabilities / confidence
(head adds head, trained_on, accuracy); score carries score /
probabilities / legend / confidence; noul carries noul / basis. Full shapes
are in Question types.
HTTP status codes at a glance
Still stuck
jev encoders: works with no torch and no network; confirms the package imports.curl http://host:port/v1/health: open even on a keyed server; tells you the encoder, itsdim, the device, the head count and the auth mode.jev heads: read stderr too, which is where unreadable head files are reported.jev.heads.broken: the same, from Python.jev ask ... --json: the raw answer shape, when the formatted output is hiding something.- Every module ships a runnable self-check:
python3 -m jevimage.client,python3 -m jevimage.server,python3 -m jevimage.training. If one of those fails, the problem is the install, not your code. All three stand up a real encoder and a real server, so they needpip install 'jevimage[serve]'; on an API-only install they exit withNo module named 'torch', which is the install working as intended. The self-check that fits that install isjev encoders.
← Cookbook · Docs index