Skip to content

FIX annotations on multi-output models, apply_pairwise without args, numpy input to characters and one_hot_to_fasta, plot_logo on non-ACGT characters, and the softmax rule under bf16 - #127

Merged
jmschrei merged 7 commits into
mainfrom
fix_annotations_pairwise_fasta_logo
Oct 19, 2026

Conversation

@jmschrei

@jmschrei jmschrei commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Six bugs on main, each fixed in its own commit with a test that fails without the fix. All six predate #120. They came up while reviewing the test suite file by file. The existing tests used inputs, or tolerances, that hid them.

Fixes

  • ablate_annotations and marginalize_annotations on models with several outputs. Both built their list of outputs with range(len(y_befores)), which counts annotations rather than model outputs. On ConvDense, which has two outputs:

    • one annotation returned only the first output;
    • three annotations raised IndexError: list index out of range.

    test_ablate_annotations_multihead and test_marginalize_annotations_multi_output passed only because they used two annotations. Both now run one, two and three annotations.

  • apply_pairwise required args in practice but defaulted it to None. Every call without args raised TypeError: 'NoneType' object is not iterable from inside the batching loop. args is now a required parameter, as it is in apply_product. Calls that pass args (the tutorial, the skill and every test do) are unchanged.

  • characters and one_hot_to_fasta on numpy arrays. Both list numpy.ndarray in their signatures, but characters called .max(dim=0) on the array and raised TypeError: _amax() got an unexpected keyword argument 'dim'. one_hot_to_fasta failed the same way through characters. Both now convert an array to a tensor first, copying it only when it is not contiguous. As a result, one_hot_to_fasta also writes the all-zero columns of an array as N, as it does for a tensor.

  • plot_logo on alphabets with characters beyond A, C, G and T. The default colors covered only A, C, G and T. A nonzero row for any other character, such as N or an amino acid, raised KeyError. With color=None, such characters are now drawn gray. A color dict that lacks a character still raises. test_plot_logo_alphabet_custom left its fifth row at zero, so it never drew N; it now draws it and checks the colors.

  • The softmax rule of deep_lift_shap and pisa under bf16 autocast. The rule computed in the dtype of the cached activations, which is bf16 under dtype=torch.bfloat16.

    • bf16 holds the differences the rule divides to about three digits, and its 1e-6 guards are below bf16's resolution.
    • On ConvSoftmax, whose attributions reach 0.062, the bf16 attributions were off from float32 by up to 0.021 on the CPU and 0.030 on CUDA.
    • The rule now computes in at least float32, and _b_hook casts the multipliers back as before.
    • Float32 and float64 activations take the same path as before: .to() returns the tensor itself when the dtype already matches.

    Largest difference from float32, before → after:

    Model CPU CUDA
    ConvSoftmax 2.1e-2 → 2.2e-4 3.0e-2 → 2.7e-4
    ConvAttentionPool 7.1e-4 → 1.6e-4 1.2e-3 → 1.6e-4
    MultiHeadAttention 4.3e-3 → 6.7e-4 7.5e-4 → 7.6e-4
    TransformerBlock (2 blocks) 1.6e-2 → 1.6e-3 3.9e-3 → 1.9e-3

    The existing autocast test compared only ConvPoolDense, to two decimals. test_deep_lift_shap_autocast_softmax_matches_full_precision checks these four models: without the fix it fails in 7 of 8 cases, all except MultiHeadAttention on CUDA.

Checks

  • Each new or changed test was run before its fix and failed with the error listed above.
  • With the fixes, the full suite passes on the CPU and on one CUDA device: 3,572 passed, 9 skipped.
  • sphinx -W passes.

The six surfaces

  1. Tests: included in each commit.
  2. Documentation:
    • The docstrings of characters and one_hot_to_fasta list numpy.ndarray.
    • plot_logo's color=None entry says that other characters are drawn gray.
    • No new page is needed.
  3. README: no snippet uses these calls.
  4. Changelog: one bullet per fix under Unreleased. The ablate and product subsections are new.
  5. Bundled skill: no change needed.
    • references/motif-effects.md already says that multi-output models return lists.
    • references/product.md and references/func-pattern.md always pass args.
  6. Compatibility: no model, state_dict or kernel is touched.

🤖 Generated with Claude Code

jmschrei and others added 6 commits October 19, 2026 11:38
The list of outputs was built by looping over the number of annotations
instead of the number of model outputs. With one annotation on a
two-output model only the first output came back, and with more
annotations than outputs it raised IndexError. The test passed only
because it used two annotations on a two-output model; it now runs one,
two and three.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The same loop bound as in ablate_annotations: the list of outputs was
built over the number of annotations instead of the number of model
outputs, so one annotation on a two-output model returned one output and
three annotations raised IndexError. The test now runs one, two and three
annotations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
args defaulted to None, but the pairing needs it: a call without args
raised TypeError: 'NoneType' object is not iterable from inside the
batching loop. args is now required, as in apply_product, so such a call
fails at the call with the missing argument named. Calls that pass args
are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both take a numpy.ndarray by their signatures, but characters called
.max(dim=0) on it and raised TypeError: _amax() got an unexpected keyword
argument 'dim', and one_hot_to_fasta failed the same way through it. Both
now convert an array to a tensor first, so one_hot_to_fasta also writes an
all-zero column of an array as N, as it does for a tensor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The default coloring covered only A, C, G and T, so a nonzero row for any
other character of `alphabet`, such as N or an amino acid, raised
KeyError. Those characters are now drawn gray. The custom-alphabet test
drew nothing for its fifth character; it now does and checks the colors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…plot_logo fixes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The rule computed in the dtype of the cached activations. Under
dtype=torch.bfloat16 those are bf16, which holds the differences it
divides to about three digits and cannot resolve its 1e-6 guards, so on
ConvSoftmax the attributions differed from float32 by up to 0.021 on the
CPU and 0.030 on CUDA, against attributions of at most 0.062. It now
computes in at least float32, and the backward hook casts the multipliers
back as before. The error falls to 2.2e-4 on the CPU and 2.7e-4 on CUDA,
and to 1.6e-3 for a two-block transformer, from 1.6e-2. Float32 and
float64 activations take the same path as before: .to() returns the
tensor itself when the dtype already matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jmschrei jmschrei changed the title FIX ablate_annotations and marginalize_annotations on multi-output models, apply_pairwise without args, numpy input to characters and one_hot_to_fasta, and plot_logo on non-ACGT characters FIX annotations on multi-output models, apply_pairwise without args, numpy input to characters and one_hot_to_fasta, plot_logo on non-ACGT characters, and the softmax rule under bf16 Oct 19, 2026
@jmschrei
jmschrei merged commit 71a3c96 into main Oct 19, 2026
4 checks passed
@jmschrei
jmschrei deleted the fix_annotations_pairwise_fasta_logo branch October 19, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant