Installation

Pick a backend: MLX on Apple Silicon, PyTorch (transformers) anywhere else — CUDA, CPU, or MPS.

pip install "jul[mlx]"        # Apple Silicon
pip install "jul[torch]"      # Linux / Windows / any GPU
pip install "jul[onnx]"       # CPU only, no torch: to deploy an exported model (AWS Lambda, containers)
pip install "jul[laya]"       # Laya, on its own runtime (PyTorch): --model laya
# add [yaml] for YAML question files, [tune] for lexical and hybrid autotune heads,
# [onnx-export] to export a model for the onnx backend

From a checkout, to work on JuL itself: pip install -e ".[dev]".

jul setup

jul setup does the rest: it picks the backend (MLX on Apple Silicon, else PyTorch), installs it if missing, downloads WeMM-Embedding-4B (2.6 GB, 4-bit) once, and checks one real decision. Running it again redoes only the check.

pip install jul
jul setup                          # --backend torch, --model minicpm5-2b, --no-install, --no-check

Let an agent install it

Or have a big LLM install the small one. Paste this into Claude Code, Codex, or any agent that has a shell:

Install jul (https://pypi.org/project/jul/), a local library that answers typed questions
with a 4B model, and check that it works on this machine.

1. Create a virtualenv with Python >= 3.10.
2. In that venv: pip install jul   then   jul setup
   jul setup picks the backend (MLX on Apple Silicon, else PyTorch), installs it, downloads
   WeMM-Embedding-4B (2.6 GB, 4-bit) and runs one test decision.
3. Then run:
   jul ask choice "Which team should handle this ticket?" -o billing:"payments, invoices" -o technical:"bugs, errors" --state "I was charged twice"
   and show me the JSON.

Do not use sudo and do not install anything outside the venv. If a step fails, show me the error and
what you suggest before trying something else. At the end, tell me in two lines: the backend, the
latency per decision, and the answer to the ticket.

Yes: a model that writes essays, installing one that answers in a single word. Neither of them minds.

Backend, device and batching

The backend defaults to MLX when it is installed on Apple Silicon, else PyTorch. Force it with TypeSafeClient(backend="torch"), jul ask ... --backend torch, or JUL_BACKEND=torch. The torch device defaults to cuda > mps > cpu (JUL_DEVICE overrides it), in bfloat16 — float32 on CPU, float16 on GPUs older than Ampere such as the T4, which have no bfloat16 tensor cores (JUL_DTYPE overrides it).

Both backends read many texts through one template in batches: compiling a question's options, a context's center, autotune and jul models add. A group holds at most JUL_BATCH_TOKENS tokens (rows × longest prompt, 16384) and JUL_BATCH_SIZE rows (64). A single call (ask) is read alone.

Models with a recurrent state (Qwen3.5 and WeMM-Embedding, whose linear-attention layers carry a state instead of a key/value cache) cannot repeat a cached prompt prefix over a batch. On PyTorch the prefix then runs again with each text, all in one right-padded batch: for WeMM-Embedding-4B on an A10G, about 150 texts/s instead of 12 one at a time, with the same vectors (cosine ≥ 0.9994). On MLX they stay one at a time behind the cached prefix: there batching gained only ×1.5 on an M4 Pro (20 to 29 texts/s) and moved the 4-bit vectors to a cosine of 0.9985, below the 0.999 jul models add requires between a cached and an uncached reading.

The two backends do not run the same weights. The MLX presets are 4-bit; PyTorch loads the original bf16 weights. On the same weights the two backends read the same vectors (cosine > 0.9999, tests/test_backends.py), but 4-bit moves them to a cosine of ~0.95 with bf16. The presets' tau and generic centers were fitted on the 4-bit MLX weights, so on PyTorch they are a starting point, not measured values. A backend-specific center is picked up from assets/<preset>.<backend>.<formulation>.center.npy when it exists. Centers, heads and calibrations saved in a context are keyed per backend, so a head trained on MLX is never applied to PyTorch vectors.

A third backend, onnx (ONNX Runtime on CPU), is never picked by default: it reads a model exported for it, and exists to deploy JuL where torch does not fit (see Deploying a fixed need). JUL_HOME moves everything JuL writes (presets, contexts, calibration data) from ~/.jul elsewhere, e.g. a read-only Lambda package.

The models

No weights are committed here. jul setup downloads them from the Hugging Face Hub into ~/.cache/huggingface, once. The CLI asks for it when they are missing; the Python API downloads them on the first call.

Preset MLX repository On disk PyTorch repository
jul-decision-wemm-4b (default) usejul/WeMM-Embedding-4B-mlx-4bit 2.6 GB tencent/WeMM-Embedding-4B (10.3 GB) + usejul/jul-decision-wemm-4b (adapters, 0.07 GB)
wemm-4b-4bit usejul/WeMM-Embedding-4B-mlx-4bit 2.6 GB tencent/WeMM-Embedding-4B
minicpm5-2b openbmb/MiniCPM5-2B-MLX 2.7 GB openbmb/MiniCPM5-2B
jul-decision-minicpm5-2b ¹ usejul/jul-decision-minicpm5-2b-mlx-4bit 1.4 GB usejul/jul-decision-minicpm5-2b

¹ A decision model, read differently from the presets: see Decision models. It is not built in; add it once with jul models add (below).

You only need the preset you actually use, and only one is ever held in memory:

jul setup                          # wemm-4b-4bit
jul setup --model minicpm5-2b

jul models shows which ones are already downloaded.

Edit this page · JuL, Just use Less · Apache-2.0 · Not affiliated with TypeSafe AI or Jev.