Testing¶
HF_HUB_OFFLINE=1 python -m pytest from the repository root runs every family's file under tests/families/ against its pinned tiny checkpoint, and the root tests under tests/. FamilySuite in tests/families/suite.py is the contract: 53 methods every family inherits, each an end-to-end statement about a standardized model. A family's file names its checkpoint, its native paths and its quirks as class attributes and adds only what is specific to it.
CUDA_VISIBLE_DEVICES="" HF_HUB_OFFLINE=1 python -m pytest tests/families/test_gpt2.py -q -p no:cacheprovider
# 42 passed, 12 skipped, 2 warnings in 5.72s
CUDA_VISIBLE_DEVICES="" HF_HUB_OFFLINE=1 python -m pytest tests/test_registry.py -q -p no:cacheprovider
# 23 passed, 7 warnings in 14.24s
The checkpoints are cached, and HF_HUB_OFFLINE=1 keeps a run from touching the Hub. The fixtures load with dispatch=True and no device=, which puts every model on a visible GPU, and CUDA_VISIBLE_DEVICES="" keeps a run on the CPU. The twelve skips on GPT-2 are the mixture-of-experts methods, which skip on a dense model. pyproject.toml sets testpaths = ["tests"] and addopts = "--dist loadgroup", so a pytest -n run with xdist keeps a class marked xdist_group on one worker. tests/conftest.py imports nnsight before anything else, since a transformers modeling module imported first can crash the interpreter on import, and pins one torch thread per xdist worker. pytest imports tests/families/suite.py as the bare module suite, and no other test helper may take that name.
What FamilySuite asserts¶
Two class-scoped fixtures load the checkpoint once per class: model, a StandardizedTransformer with dispatch=True, attn_implementation="eager" plus LOAD_KWARGS, and raw_model, the same checkpoint as a plain TransformersModel. The methods, by what they pin:
| group | methods | assert |
|---|---|---|
| names | test_family_resolved, test_standard_names_alias_native_envoys, test_no_inner_model_alias, test_every_layer_is_renamed, test_envoy_classes, test_trace_through_standard_names |
model.family is FAMILY; every NATIVE row resolves to the native envoy itself; nothing is bound at model.model; every block carries the block rows; every block, attention, MLP and mixer is exactly the family's class and subclasses the component base; a trace through the standard names gives hidden_size and vocab_size widths |
| availability | test_support_lists_every_standard_value, test_support_is_what_this_family_expects, test_support_matches_what_reads |
support() has exactly the expected keys; each EXPECTED_UNAVAILABLE key carries its reason substring on every block and every other key is None; on block 0 an available name is an EProperty and an unavailable one raises Unavailable with that reason |
| boundaries | test_layer_output_is_the_tensor, test_layer_input_is_the_block_input, test_every_value_reads_a_tensor_on_every_layer, test_contribution_identity, test_sublayer_inputs_are_the_normed_stream, test_module_boundaries, test_sublayer_interiors, test_renamed_model_equals_raw_model, test_boundary_writes_land |
layer_output equals .output or its first element; layer_input + attention_output + mlp_output == layer_output on every block at 8 ulp of the block's dtype; what enters each sublayer is ATTENTION_NORM's and MLP_NORM's output; layer_mid, the norm outputs, attention_premix, mlp_activation and mlp_neurons are the module boundaries they name and a write to each moves the logits; the renamed model equals the raw one; assigning and zeroing the three boundary values moves the logits |
| the pattern | test_probabilities_are_a_pattern, test_attention_interior_under_generate, test_pattern_across_layers_and_traces, test_written_pattern_moves_the_logits, test_every_source_value_resolves_on_every_layer |
[batch, num_heads, seq, seq], the model dtype, rows summing to one (or below one with ATTENTION_SINK), lower-triangular; a decode step's pattern has one query row; two traces agree; a written pattern and a zeroed head move the logits; every value with inside_forward() reads on every attention block |
| the interior | test_interior_shapes, test_interior_writes_are_causal, test_interior_in_place_edits |
the queries, keys, values, scores and head outputs have their layouts and softmax(scores) is the pattern (pattern_from_scores is overridable); zeroing each moves the logits and zeroed head outputs leave a position-independent contribution; in-place edits land, or queries raise RuntimeError mentioning view with REFUSES_IN_PLACE_QKV |
| methods | test_skip_layers_hands_the_stream_straight_through, test_skip_layers_with_a_given_stream, test_steer, test_project_on_vocab_is_the_logit_lens |
skipping blocks 1 to the last leaves block 0's stream as the final one; skip_with zeros reach the next block; steer moves the last position by 2 * vector and no other; project_on_vocab on the last block's output is logits |
| layouts | test_values_match_their_annotations |
every value with a layout on the root, a block, its attention, its MLP, its mixture and its mixer is an instance of the alias and each named axis matches axis_sizes |
| the input | test_input_accessors, test_attention_mask_under_generate, test_assigning_input_ids_runs_other_ids |
input_ids, input_size and attention_mask are [1, n]; a decode step's mask covers the cache; assigning another prompt's ids reproduces its logits and assigning input_size raises |
| the root | test_logits_are_the_models_output, test_assigning_logits_replaces_the_result, test_token_embeddings_are_the_embedding_output, test_next_token_probs, test_sizes_match_the_model, test_per_module_sizes_match_each_block, test_repr_lists_the_values |
logits is output.logits; assigning it zeros tracer.result.logits; next_token_probs is the last position's softmax and refuses assignment; the root sizes match the pattern, the stream and the projections, and each block's own sizes match its tensors; the block repr lists the values |
| a mixture | the twelve test_moe_*, test_expert_*, test_routed_*, test_writes_to_the_shared_*, test_zeroing_*, test_ablating_*, test_rerouting_*, test_writing_router_logits_* methods |
on the first block with an mlp that is a Moe running a mixture: the routing pair's shapes, expert_outputs summing to routed_output, routed_output + shared_expert_output being the module's output, a zeroed slot and an ablated expert changing exactly the tokens they touch, a rerouted slot matching a hand computation, written router logits deciding the routing, and per-invoke rows; skipped on a dense model |
Reads in one trace follow the forward, and families bind the interior values at different points, so the interior and layout tests read one value per trace. Containers filled inside a trace block are bound outside it, since a name first bound in the block does not survive it.
How a family file adds specifics¶
Most files are the class attributes alone. tests/families/test_gpt2.py sets REPO, FAMILY, NATIVE = rows("transformer", "h", "wte", "ln_f", attn="attn", ln1="ln_1", ln2="ln_2") and REFUSES_IN_PLACE_QKV = True, and adds one test that flipping reorder_and_upcast_attn on the config makes the interface unavailable. rows(container, layers, embed, norm, ...) builds the standard-to-native map, and LLAMA_ROWS is the Llama layout.
| attribute | states |
|---|---|
EXPECTED_UNAVAILABLE |
support() key to a substring of the reason, for the values this checkpoint lacks; a module no block has is absent from support() and is not listed here |
ATTENTION_NORM, MLP_NORM, MLP_NORM_BEFORE_ATTENTION |
which block module's output enters each sublayer (None when the block input does), and whether the MLP's norm fires first |
REFUSES_IN_PLACE_QKV, ATTENTION_SINK, KV_HEADS_EXPANDED, QUERY_GATED |
queries that are split views, a sink column, keys and values already expanded to num_heads, a q_proj twice as wide |
MLP_WIDTH_KEY, LOAD_KWARGS |
a config key naming the MLP width when it is not intermediate_size; extra load arguments (DBRX needs dtype=torch.float32) |
MOE_UNAVAILABLE, ROUTER_EXTRA_CLASSES |
mixture values the mixture lacks; router classes beyond the experts |
DECODE_MASK_COVERS_CACHE, MID_IDENTITY, PREMIX_IS_HEAD_OUTPUTS, NEURONS_FROM_ACTIVATION |
a pure state-space model's decode mask, a gated stream, a norm between the heads and the output projection, a fused gate_up_proj |
A family with a checkpoint that needs another config or another kernel path makes a second class rather than mutating the shared fixture. test_falcon.py has three classes for the 7B layout, the 7B layout with alibi written into a copy of config.json, and the 40B layout, and test_olmo3.py symlinks the snapshot into a temporary directory and rewrites only config.json. The hybrid files override test_every_layer_is_renamed for the per-block self_attn and linear_attn split. The Mamba-1 files mix SelectiveScanSuite (tests/families/scan_suite.py) in before FamilySuite: A class-scoped autouse fixture routes the family to the pure-torch kernels for the class and restores the default afterwards, which matters because the routing is process-wide. The Mamba-2 files take their mixer tests from tests/families/ssd.py. A multimodal wrapper runs the whole FamilySuite loaded with LOAD_KWARGS = {"task": "image-text-to-text"}, and its vision side runs VisionSuite from tests/families/vision_suite.py: the tower names, the tower's identity, layers[0].input[vision.image_token_mask] == vision.image_features, causal edits, and no vision host on a text-only checkpoint or load. tests/families/test_default.py runs the default family on several shipped families' checkpoints with the contributions listed unavailable, and checks that every value it serves is bit-identical to the dedicated family's.
The root tests¶
CUDA_VISIBLE_DEVICES="" HF_HUB_OFFLINE=1 python -m pytest tests/test_base.py tests/test_prompt_utils.py tests/test_nnsight_utils.py tests/test_order.py -q -p no:cacheprovider
# 51 passed in 26.67s
| file | covers |
|---|---|
tests/test_registry.py |
every module under nnterp/families/ is named after its type; import nnterp pulls in no transformers.models.*.modeling_* module and lookup("gpt2") imports only that family, in a subprocess; an unknown model_type falls back to the default family with a warning; register adds and overrides; a preloaded module uses its own config; a caller's rename and envoys merge over the family's; the remote key is TransformersModel; a size or project_on_vocab defined on a family wins over the root's; family= takes a module or a SimpleNamespace for one load; a size refuses assignment |
tests/test_base.py |
a tuple-returning block is unwrapped and rewrapped; an unavailable(...) marker is listed, raises on read and makes hasattr raise; paths with ../, / and select functions resolve and write; route_kernels round-trips a module's bindings; per_call separates invokes and replays an edit; axis_size resolves by name up the tree; probs_to_dict keeps tokens that decode alike |
tests/test_prompt_utils.py, tests/test_nnsight_utils.py |
the helpers, on GPT-2 with left padding |
tests/test_order.py |
the measured forward order and its ranks |
tests/test_encyclopedia.py |
every encyclopedia entry builds from its pinned checkpoint |
tests/vllm_families/ |
the vLLM engine against the transformers one; collected only where vLLM imports and a GPU is visible |
The procedure for a transformers release that moves an operation name is on transformers compatibility.