Harvest Activations
A harvest reads the residual stream of a Base model at the Layers that you choose, for each token of many prompts. Inference writes it to files that you download, for example to train a sparse autoencoder or a probe.
Set ACS_API_BASE and ACS_API_KEY first, as Set up shows. A harvest runs in the background on GPUs of its own, apart from Completions. Harvests in Limits gives every number on this page. Download a harvest from a shell script does each step from one script.
Submit a harvest
-
Send the model, the prompts, and the Layers. This harvest reads Layers 8, 16, and 24 of two prompts:
curl "$ACS_API_BASE/harvest" \ -H "Authorization: Bearer $ACS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "llama-8b", "prompts": ["The sky is", "Grass is"], "layers": [8, 16, 24]}'The API answers 202 with the harvest. The answer has its
job_id,"status": "pending", the model,n_prompts, the Layers that it reads, andreserved_tokens.tokens,started_at, andcompleted_atarenulluntil the harvest starts or ends. -
Put the harvest's
job_idin a shell variable for the next commands, for exampleexport JOB_ID=<JOB_ID>with thejob_idfrom the answer. The Python programs below read it too.
The request takes these fields. An unknown field gets 400.
| Field | What it does |
|---|---|
model | The Base model. Harvests on each model lists the models that harvest. |
prompts | A list of texts, each of 1 or more characters. Token ids are not taken. A text that holds half of a surrogate pair, such as a lone \ud800 in JSON, gets 400. |
layers | A list of Layers in any order, or "all". The files hold each Layer once, in ascending order. Left out, the harvest reads the quartiles of the model's Layers. |
shard_size | The prompts in each file. |
add_special_tokens | true by default: the tokenizer adds the model's start token to each prompt. Set false for text that already starts with one. |
project_onto | Directions to project the residual stream onto, in place of the stream itself (Project onto directions). |
batch_size | Kept in the manifest only. Inference sizes its own batches. |
The API checks the request and the caps when you submit it. If it refuses the harvest, it creates nothing and answers the error object:
- 400 for a field that is not valid.
paramnames the field, such asprompts,layers, orproject_onto. A Layer that the model does not have gets a message such asmeta-llama/Llama-3.1-8B has layers 0 to 31: it has no layer 40. - 400
harvest_unsupportedfor a model that the API serves but does not harvest. - 400
model_not_foundfor a model that the API does not serve. - 413 for a request body over the harvest's own size cap.
- 429
harvest_quota_exceeded,harvest_concurrency_exceeded, orharvest_capacity_exceededwhen a cap is full. The message names the cap, such asYou have 3 harvests on small models queued or running, your most. Submit another when one ends, or cancel one. - 429
budget_exceededwhen your tokens cannot hold the prompts. - 429
spend_capwhen the API has paused new harvests to limit GPU spending. ItsRetry-Afterheader says when to try again.
A prompt longer than the model's context keeps its first tokens, and the rest is cut. Harvests in Limits says how many.
Check on the harvest
Read the harvest. With wait, the API answers when the status changes, or after that many seconds:
curl "$ACS_API_BASE/harvest/$JOB_ID?wait=60" \
-H "Authorization: Bearer $ACS_API_KEY"Read it again until its status is done, failed, or cancelled. A harvest is in one of these states:
status | Meaning |
|---|---|
pending | The harvest waits for the API to start it. |
running | The API holds the prompts' tokens, and Inference runs the harvest on its GPUs. |
done | The files are ready to download. The harvest has its result. |
failed | The harvest did not complete. error says why (Handle a failed harvest). |
cancelled | You or an admin canceled the harvest before it ended. |
The times are ISO 8601 in UTC. created_at is when you submitted the harvest, and started_at is when its GPU run started. completed_at is when it was done, failed, or was canceled.
reserved_tokens is what the harvest holds while it runs, and tokens is what it was charged, once it ended. Holds of background work explains both.
When too many reads wait at once (Harvests in Limits), the API answers at once without waiting. That answer has the headers X-Acs-Longpoll: declined and Retry-After: 3. Wait 3 seconds, then read again.
Download the files
-
Wait until the harvest's
statusisdone. -
Read the harvest:
curl "$ACS_API_BASE/harvest/$JOB_ID" \ -H "Authorization: Bearer $ACS_API_KEY"Its
resulthas a link to each file, andurls_expire_atsays when the links stop working:Field of resultWhat it is shard_urlsOne link for each shard file, in order. stats_urlThe link to stats.safetensors.manifest_urlThe link to manifest.json.n_shards,layer_indices,contentsHow many shards there are, the Layers in them, and activationsorprojections.tokens,bytes_writtenThe token positions in the files, start tokens included, and their size in bytes. timingsThe seconds of the GPU run: load_sto load the model,forward_sto read the stream,save_sto write the files, andupload_sto upload them. -
Download the shards and read the first one. This program needs
requests,safetensors, andtorch:import os import requests from safetensors.torch import load_file API_BASE = os.environ["ACS_API_BASE"] HEADERS = {"Authorization": f"Bearer {os.environ['ACS_API_KEY']}"} JOB_ID = os.environ["JOB_ID"] answer = requests.get(f"{API_BASE}/harvest/{JOB_ID}", headers=HEADERS, timeout=60) answer.raise_for_status() result = answer.json()["result"] for index, url in enumerate(result["shard_urls"]): with requests.get(url, stream=True, timeout=600) as shard: shard.raise_for_status() with open(f"shard_{index:05d}.safetensors", "wb") as file: for chunk in shard.iter_content(chunk_size=1 << 20): file.write(chunk) tensors = load_file("shard_00000.safetensors") print(tensors["prompt_0"].dtype, tuple(tensors["prompt_0"].shape)) print(tensors["tokens_0"].tolist())The program prints the type and shape of the first prompt's tensor, then its token ids.
A link needs no API key, and works until urls_expire_at. Each answer of GET /v1/harvest/{job_id} for a done harvest makes new links, and the API keeps the files. Kept data gives the times.
Anyone who has a link can download that file until the link expires. Keep the links as private as your API key, and read the harvest again for new ones instead of sharing them.
A harvest that is not done has no result and no urls_expire_at.
Read the shards
Each shard holds shard_size prompts, in the order of prompts. For the prompt at index i of your prompts, a shard has two tensors:
| Tensor | Type and shape |
|---|---|
prompt_<i> | bfloat16, [Layers, tokens, hidden]. With project_onto, float32, [Layers, tokens, directions]. |
tokens_<i> | int32, [tokens]: the token ids, in the order of the token axis of prompt_<i>. |
The Layer axis follows layer_indices, in ascending order. Layer k is the output of decoder block k, before the model's final norm, as for a Completion (The residual stream). The last Layer is before the final norm too. tokens_<i> starts with the start token when add_special_tokens is true. The token ids let you match each row to its token without the tokenizer.
Avoid a second start token
The tokenizer adds the model's start token, such as <|begin_of_text|>, in front of each prompt. If your text already starts with that token, the prompt gets two, and the API does not warn you. Then tokens_<i> is one longer than your own tokenization and repeats its first id.
Send "add_special_tokens": false for text that already starts with the start token. The tokenizer then reads each prompt as it is.
Read the manifest and the statistics
manifest.json describes the whole harvest. Inference writes it last, so a harvest whose manifest exists has every file. It has the old platform's file layout, version 2, so programs that read those files read these too. Its main keys:
| Key | What it is |
|---|---|
run_id | The harvest's job_id. |
layout_version | 2. |
model_id, model_identity | The model, and the Hugging Face revision of the weights that ran. |
runtime | The versions and settings that ran the model. |
dtype, contents | bfloat16 and activations, or float32 and projections. |
add_special_tokens, layer_indices, residual_stream_convention | The request's settings, the Layers, and what Layer k means. |
prompts | The text of every prompt, in order. |
shards | For each shard, its file, the indexes of its prompts, and the name and shape of each tensor. |
mean_token_norm | For each Layer, the mean length of a token's vector. |
stats.safetensors holds mean and std (float32, [Layers, hidden]), and layer_indices. The statistics count every token position but the first of each prompt. With add_special_tokens: true, that first position is the start token, whose vectors are outliers.
This program reads both. It needs requests, safetensors, and torch:
import os
import requests
from safetensors.torch import load
API_BASE = os.environ["ACS_API_BASE"]
HEADERS = {"Authorization": f"Bearer {os.environ['ACS_API_KEY']}"}
JOB_ID = os.environ["JOB_ID"]
answer = requests.get(f"{API_BASE}/harvest/{JOB_ID}", headers=HEADERS, timeout=60)
answer.raise_for_status()
result = answer.json()["result"]
manifest = requests.get(result["manifest_url"], timeout=60).json()
print(manifest["layer_indices"], manifest["residual_stream_convention"])
stats = load(requests.get(result["stats_url"], timeout=60).content)
print(stats["mean"].shape, stats["std"].shape)The manifest holds your prompts' text, so keep it as private as the prompts.
Project onto directions
With project_onto, Inference writes the residual stream projected onto your directions, in place of the stream itself. The files are then much smaller. Inference scales each direction to length 1 first. Each direction has the model's capabilities.hidden_size, from List the models.
-
Put your directions, such as a probe's weights, in a 2-D array of
[directions, hidden_size]. -
Send its raw bytes, little-endian, in base64 in
data, withdtypeandshape. This program submits 8 random directions ofllama-8b:import base64 import os import numpy as np import requests API_BASE = os.environ["ACS_API_BASE"] HEADERS = {"Authorization": f"Bearer {os.environ['ACS_API_KEY']}"} directions = np.random.default_rng(0).standard_normal((8, 4096)).astype("<f4") body = { "model": "llama-8b", "prompts": ["The sky is", "Grass is"], "layers": [8, 16, 24], "project_onto": { "data": base64.b64encode(directions.tobytes()).decode("ascii"), "dtype": "float32", "shape": list(directions.shape), "compression": "none", }, } answer = requests.post(f"{API_BASE}/harvest", headers=HEADERS, json=body, timeout=60) answer.raise_for_status() print(answer.json()["job_id"]) -
Put the printed
job_idinJOB_ID, and download the files as before.
dtype is float32, float16, or bfloat16. For float16, use astype("<f2"). compression takes only none, and you can leave it out. Each prompt_<i> is then float32, [Layers, tokens, directions]. The manifest has contents: "projections" and a projection key, and the statistics describe the projections.
The API refuses directions that do not fit before any GPU starts. Directions of another hidden size get 400 with param project_onto. These get 400 too, each with its message:
| Directions | Message |
|---|---|
| Bytes that do not match the shape | project_onto.data is 8 bytes; the shape 1 × 3 of float32 needs 12. |
| A number that is not finite | project_onto has numbers that are not finite. |
| A direction that is all zeros | project_onto has a direction whose length is 0. |
A character in data that is not base64, a space, a tab, or a line break | project_onto.data is not valid base64. |
Cancel a harvest
You can cancel a harvest that is pending or running, as it is while Inference loads the model.
-
Submit the harvest:
curl "$ACS_API_BASE/harvest" \ -H "Authorization: Bearer $ACS_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model": "llama-8b", "prompts": ["The sky is"], "layers": "all"}' -
Put the new
job_idinJOB_ID, then cancel the harvest:curl -X DELETE "$ACS_API_BASE/harvest/$JOB_ID" \ -H "Authorization: Bearer $ACS_API_KEY"The API answers 200 with the harvest,
"status": "cancelled".
The API stops the harvest's GPU run, also one that is still starting. A harvest that you cancel while it is running and its GPU run started is charged the tokens that it holds, as tokens shows. Any other cancel costs nothing:
- Before its GPU run starts.
- While it waits
pendingagain after an API restart. - After the API process that ran it stopped without warning.
A second cancel answers the canceled harvest again. A cancel of a done or failed harvest gets 409 harvest_not_cancellable, and a job_id that you do not have gets 404 harvest_job_not_found.
Handle a failed harvest
A failed harvest is charged no tokens. Its error is a message that says why:
| Why it failed | What to do |
|---|---|
| Inference did not start the harvest. It does not count toward your harvests of the month. | Submit it again later. |
| The GPU run did not end within its time limit (Time limits). | Split the prompts into smaller harvests, or read fewer Layers. |
| Inference no longer had the result of the GPU run. | Submit it again. |
| Inference could not confirm that the model's weights are the pinned revision, so it ran nothing. | Submit it again later, and report it if it repeats. |
| Inference refused a value of the request. | Fix what error names. |
| When the harvest started, your tokens could no longer hold its prompts. | Ask for more tokens, or submit fewer prompts. |
| The API key was revoked, or your access was suspended, before the harvest started. | Submit it with a key that works. |
| Another failure. | Report it with the harvest's job_id. |
If the API restarts while your harvest runs, the harvest goes back to pending, and its GPU run goes on. The API then follows the same GPU run, and charges the tokens once. When the API restarts says how.
The API reference lists every field of each harvest route.