Generating images in client-keyed mode

This page gives the complete procedure for generating Z-Image images on a rented GPU while the model key, the tokenizer and the VAE stay on the user’s machine. The cloud runs the text encoder and the diffusion transformer without a key and returns the final latents encrypted to the requester. Z-Image describes the scheme, and Client-keyed mode the client-keyed mode in general.

Roles and requirements

Machine

Holds

Install

User

Key file, the complete sealed pipeline (including trusted.kllm, the tokenizer and the VAE)

pip install -e '.[client,image]'

Cloud

The -cloud directory only: text encoder and DiT weights and configs, no key, no tokenizer, no VAE

pip install -e '.[image]'

The image extra installs diffusers>=0.36, transformers, FastAPI, Uvicorn and Pillow on top of seal. The user’s VAE decodes on CUDA, Apple MPS or the CPU.

Supported pipelines: Z-Image text-to-image with 2x2 latent patches. Image-conditioned variants and text encoders with attention biases are refused.

Procedure

  1. Seal on the user’s machine. Create a key if none exists, then seal with --client. --transform accepts orth (the default) or perm.

    $ kllm keygen
    $ kllm seal ~/models/Z-Image -o ~/models/Z-Image-sealed --client
    

    The command writes the user’s copy to Z-Image-sealed and the cloud’s part to Z-Image-sealed-cloud (--cloud DIR selects another location). Sealing the 20 GB pipeline took 57 s.

  2. Upload only the cloud’s part.

    $ rsync -a ~/models/Z-Image-sealed-cloud CLOUD:models/
    
  3. Serve in the cloud without a key, bound to the loopback interface:

    $ kllm serve models/Z-Image-sealed-cloud --client --host 127.0.0.1 --port 30010
    

    For a client-keyed pipeline, kllm serve --client starts kLLM’s latent server, python -m kllm.keyed.client.server, instead of an engine; --engine does not apply. Arguments after -- go to the latent server, which accepts --device (default cuda) and the request defaults --size, --steps and --guidance-scale.

  4. Open a tunnel from the user’s machine:

    $ ssh -N -L 30010:127.0.0.1:30010 user@CLOUD
    
  5. Generate on the user’s machine:

    $ kllm client generate ~/models/Z-Image-sealed "A red fox standing in fresh snow at dawn" \
        --url http://127.0.0.1:30010 -o fox.png
    

    The command prints the output path, the cloud’s generation time and the device that decoded the latents. --negative-prompt, --size (default 1024x1024), --steps (28), --guidance-scale (4.0), --seed (0) and --device set the generation; see Z-Image for the full option table.

Warning

The latent server performs no authentication, does not bound steps or size, and kllm serve binds 0.0.0.0 unless --host is given. The reply encryption has no server identity, so an active intermediary on a plain-HTTP path can substitute its own public key. Keep the server on the loopback interface behind an SSH tunnel, or behind an authenticating TLS gateway.

What crosses the network

kllm client generate renders the prompt with the pipeline’s chat template, encodes it with the permuted tokenizer (at most 512 token ids) and sends a request to /v1/latents:

  • the permuted prompt ids and, when the guidance scale is above 0, the permuted ids of the negative prompt (an empty prompt if none is given);

  • the size, number of steps, guidance scale and seed;

  • a fresh X25519 public key generated for this request.

The server runs the sealed text encoder and the stock sampler in the key’s latent basis, serializing requests, and encrypts the final latents to the request’s public key with its own one-off X25519 key (ECDH, HKDF-SHA256, AES-256-GCM). The response carries the ciphertext, and in the clear the latent tensor shape and the generation time. The server never loads a tokenizer or a VAE, writes nothing to disk and runs without request logging.

The client decrypts the latents, maps them back from the key’s patch basis and decodes them with the public VAE.

Measured results

Z-Image sealed with orth, 1024x1024, 28 steps, guidance 4, three prompts by two seeds from the same noise, against the plain diffusers pipeline on the same NVIDIA A40 (benchmarks/zimage_client.py):

Latent relative error

PSNR

End to end

Plain pipeline

48.4 s

Plain, DiT attention on PyTorch’s math kernel (noise floor)

7.2%

33.9 dB

--client (cloud, then VAE on the same GPU)

11.5%

31.6 dB

48.1 s

Every pair showed the same scene, subject and composition. On the two prompts where the noise floor itself was large, the client run drifted further; objects moved slightly. Sealing under perm drifted as much. The drift comes from the DiT’s input and output layers, which are rounded to bf16 again in the key’s patch basis, not from the text encoder.

The encrypted reply carries 1 MB of float32 latents for a 1024x1024 image (1.4 MB in base64). VAE decode times on the user’s side (160 MB VAE):

Device

Decode time

NVIDIA A40

0.27 s

Apple M3, 16 GB, MPS, fp16

3.1 s

Apple M3, 16 GB, MPS, fp32

4.6 s

Apple M3 CPU

148 s

The first image requested from the Apple M3 through an SSH tunnel took 70 s, including unlocking the key and loading the tokenizer and the VAE; 49 s of that was the cloud. These decode timings have no script in the repository.

What this protects

Protected: what the cloud’s software records or passes along. The request body holds permuted token ids, and the response body holds ciphertext. The size, number of steps, guidance scale, seed, latent shape and generation time are in the clear.

Not protected, and not claimed:

  • The image in GPU memory. The latents in the server’s GPU memory show the image; a principal component analysis of the cloud’s patch vectors, without the key, recovers a clear thumbnail. This is why the response is encrypted.

  • The DiT’s activations. Only five DiT tensors are re-based (the patch embedding, the final layer and the caption embedder); every block works in canonical coordinates.

  • The prompt against frequency analysis. Token ids are under a fixed permutation, which is a substitution cipher. The text encoder is a public Qwen3 checkpoint, and its sealed embedding has the structure that the measured weight attack exploits, so the permutation is expected to be recoverable from the cloud’s copy with the public weights (Weights of a public model, or of a fine-tune of one; measured on Qwen3-8B, not on the Z-Image encoder).

  • The serving process. A process that is attached to or modified sees everything it computes.

The figure in What this mode protects shows what the cloud holds and the thumbnail that the principal component analysis recovers. The full list of what client-keyed mode does not protect is in Not protected in client-keyed mode.

Z-Image without --client

kllm seal without --client writes a Z-Image pipeline under a separate demonstration scheme (d1-v1) that ignores --transform and leaves the text encoder, VAE and tokenizer unchanged. kllm serve --key refuses such a pipeline with <path> uses scheme 'd1-v1'; reseal it with this kLLM; only kllm demo serves it. For generation on a rented GPU, seal with --client as above.

Note

kllm demo up is under repair in 0.1.0.dev0 and does not start.

See also