icalens 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,32 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ name: Build and publish
13
+ runs-on: ubuntu-latest
14
+ environment:
15
+ name: pypi
16
+ url: https://pypi.org/p/icalens
17
+ permissions:
18
+ id-token: write
19
+ steps:
20
+ - name: Check out repository
21
+ uses: actions/checkout@v4
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@v7
25
+ with:
26
+ enable-cache: true
27
+
28
+ - name: Build distributions
29
+ run: uv build
30
+
31
+ - name: Publish distributions to PyPI
32
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ .venv/
2
+ dist/
3
+ build/
4
+ *.egg-info/
5
+ __pycache__/
6
+ *.py[cod]
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .coverage
11
+ htmlcov/
12
+ demo/output/
icalens-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sida Liu and Feijiang Han
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
icalens-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,74 @@
1
+ Metadata-Version: 2.4
2
+ Name: icalens
3
+ Version: 0.1.0
4
+ Summary: Fit, share, and apply ICA lenses for language-model activations.
5
+ Project-URL: Homepage, https://liusida.github.io/ica-lens-paper/
6
+ Project-URL: Repository, https://github.com/liusida/icalens
7
+ Author: Sida Liu, Feijiang Han
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ License-File: THIRD_PARTY_NOTICES.md
11
+ Keywords: ICA,activations,interpretability,language-models
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Requires-Python: >=3.10
17
+ Requires-Dist: huggingface-hub>=0.25
18
+ Requires-Dist: numpy>=1.24
19
+ Requires-Dist: safetensors>=0.4
20
+ Requires-Dist: torch>=2.1
21
+ Requires-Dist: tqdm>=4.66
22
+ Description-Content-Type: text/markdown
23
+
24
+ # ICA Lens
25
+
26
+ ICA Lens fits, shares, and applies Independent Component Analysis bases for
27
+ language-model activations. Version 0.1 operates on activations supplied by the
28
+ caller; it does not load language models or capture activations.
29
+
30
+ ```bash
31
+ uv add icalens
32
+ ```
33
+
34
+ Load a published lens:
35
+
36
+ ```python
37
+ from icalens import ICALens
38
+
39
+ lens = ICALens.from_pretrained("liusida/icalens-gpt2-small")
40
+ scores = lens.transform(activations, layer=6)
41
+ reconstructed = lens.inverse_transform(scores, layer=6)
42
+ ```
43
+
44
+ Fit and publish your own:
45
+
46
+ ```python
47
+ from icalens import ICALens
48
+
49
+ lens = ICALens(
50
+ base_model="openai-community/gpt2",
51
+ base_model_revision="FULL_COMMIT_HASH",
52
+ activation_site="resid_post",
53
+ )
54
+ lens.fit(activations, layer=6, random_state=0)
55
+ lens.save("./my-icalens")
56
+ lens.push_to_hub("username/icalens-gpt2-small")
57
+ ```
58
+
59
+ Inputs may be NumPy arrays or PyTorch tensors. Leading dimensions are treated
60
+ as sample dimensions and the final dimension must be the model hidden size.
61
+ Fitting uses ICA Lens's built-in PyTorch FastICA implementation and can run on
62
+ the input tensor's device. NumPy inputs are fitted on CPU. ICA Lens does not
63
+ depend on scikit-learn or SciPy.
64
+
65
+ See [`docs/api.md`](docs/api.md) and
66
+ [`docs/artifact-format.md`](docs/artifact-format.md) for the initial API and
67
+ portable artifact format.
68
+
69
+ For the 1,000-token GPT-2/Pile-10k fitting demo, run:
70
+
71
+ ```bash
72
+ uv sync
73
+ uv run python demo/fit.py
74
+ ```
@@ -0,0 +1,51 @@
1
+ # ICA Lens
2
+
3
+ ICA Lens fits, shares, and applies Independent Component Analysis bases for
4
+ language-model activations. Version 0.1 operates on activations supplied by the
5
+ caller; it does not load language models or capture activations.
6
+
7
+ ```bash
8
+ uv add icalens
9
+ ```
10
+
11
+ Load a published lens:
12
+
13
+ ```python
14
+ from icalens import ICALens
15
+
16
+ lens = ICALens.from_pretrained("liusida/icalens-gpt2-small")
17
+ scores = lens.transform(activations, layer=6)
18
+ reconstructed = lens.inverse_transform(scores, layer=6)
19
+ ```
20
+
21
+ Fit and publish your own:
22
+
23
+ ```python
24
+ from icalens import ICALens
25
+
26
+ lens = ICALens(
27
+ base_model="openai-community/gpt2",
28
+ base_model_revision="FULL_COMMIT_HASH",
29
+ activation_site="resid_post",
30
+ )
31
+ lens.fit(activations, layer=6, random_state=0)
32
+ lens.save("./my-icalens")
33
+ lens.push_to_hub("username/icalens-gpt2-small")
34
+ ```
35
+
36
+ Inputs may be NumPy arrays or PyTorch tensors. Leading dimensions are treated
37
+ as sample dimensions and the final dimension must be the model hidden size.
38
+ Fitting uses ICA Lens's built-in PyTorch FastICA implementation and can run on
39
+ the input tensor's device. NumPy inputs are fitted on CPU. ICA Lens does not
40
+ depend on scikit-learn or SciPy.
41
+
42
+ See [`docs/api.md`](docs/api.md) and
43
+ [`docs/artifact-format.md`](docs/artifact-format.md) for the initial API and
44
+ portable artifact format.
45
+
46
+ For the 1,000-token GPT-2/Pile-10k fitting demo, run:
47
+
48
+ ```bash
49
+ uv sync
50
+ uv run python demo/fit.py
51
+ ```
@@ -0,0 +1,23 @@
1
+ # Third-party notices
2
+
3
+ The internal FastICA implementation in `src/icalens/_fastica.py` is adapted
4
+ from [FastICA_torch](https://github.com/liusida/FastICA_torch), copyright 2024
5
+ Richard Hakim, under the MIT License:
6
+
7
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ > of this software and associated documentation files (the "Software"), to deal
9
+ > in the Software without restriction, including without limitation the rights
10
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ > copies of the Software, and to permit persons to whom the Software is
12
+ > furnished to do so, subject to the following conditions:
13
+ >
14
+ > The above copyright notice and this permission notice shall be included in all
15
+ > copies or substantial portions of the Software.
16
+ >
17
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ > SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # ICA Lens fitting demo
2
+
3
+ Install the development dependencies and fit a lens from 1,000 Pile-10k tokens:
4
+
5
+ ```bash
6
+ uv sync
7
+ uv run python demo/fit.py
8
+ ```
9
+
10
+ By default, the demo:
11
+
12
+ 1. Streams text from `NeelNanda/pile-10k`.
13
+ 2. Builds a 1,000-token candidate pool from independently tokenized documents.
14
+ 3. Uses all 1,000 positions for fitting by default.
15
+ 4. Preserves each sampled token's original left context during activation capture.
16
+ 5. Loads `openai-community/gpt2` on CUDA through `gb10-load-llm`.
17
+ 6. Captures `outputs.hidden_states[7]`, recorded as ICA Lens layer 6 after
18
+ excluding the initial embedding state.
19
+ 7. Fits a full 768-component ICA Lens.
20
+ 8. Saves it under `demo/output/icalens-gpt2-small-1k`.
21
+
22
+ By default, `--candidate-tokens` equals `--token-budget`. Set it explicitly to
23
+ sample the fitting tokens from a larger pool:
24
+
25
+ ```bash
26
+ uv run python demo/fit.py \
27
+ --candidate-tokens 10000 \
28
+ --token-budget 1000 \
29
+ --max-iter 1000 \
30
+ --seed 0
31
+ ```
32
+
33
+ `--max-iter` controls the fixed number of FastICA fixed-point iterations.
34
+ The fitting demo displays a tqdm bar with the current convergence limit and
35
+ mean contrast objective (`obj`). ICA Lens deliberately runs exactly
36
+ `--max-iter` iterations: the classical FastICA tolerance criterion is treated
37
+ as diagnostic-only because it is not an appropriate stopping rule in the LLM
38
+ activation regime. The displayed objective uses the selected FastICA contrast:
39
+ `log(cosh(x))`, `-exp(-x²/2)`, or `x⁴/4`.
40
+
41
+ The demo also displays token-rate progress bars while building the Pile-10k
42
+ candidate pool and capturing the sampled GPT-2 activations.
43
+
44
+ To test whether a fit stays within an ordinary 16 GiB GPU budget, cap the
45
+ PyTorch CUDA allocator:
46
+
47
+ ```bash
48
+ uv run python demo/fit.py \
49
+ --layers 6 \
50
+ --token-budget 1000000 \
51
+ --fit-batch-size 8192 \
52
+ --max-vram-gb 16
53
+ ```
54
+
55
+ The script reports peak PyTorch CUDA memory at the end. This cap covers
56
+ allocations managed by PyTorch, but not CUDA context memory or allocations made
57
+ directly by non-PyTorch libraries, so it is a close OOM test rather than a full
58
+ hardware emulator.
59
+
60
+ Selected activations are retained in CPU memory. Whitening statistics,
61
+ FastICA updates, and final source scaling are computed in repeated CUDA batches,
62
+ so GPU memory scales with `--fit-batch-size` rather than the total token count.
63
+
64
+ Fit several layers with a comma-separated list:
65
+
66
+ ```bash
67
+ uv run python demo/fit.py --layers 0,6,11
68
+ ```
69
+
70
+ Use `--layers all` to fit all 12 GPT-2 transformer blocks. The default single
71
+ layer is intended as a quick end-to-end check; a full published artifact should
72
+ use a larger, explicitly sampled token corpus.
73
+
74
+ The demo requires network access for Hugging Face downloads and a CUDA device.
75
+
76
+ ## Apply the saved lens
77
+
78
+ After fitting layer 6, apply it to fresh text:
79
+
80
+ ```bash
81
+ uv run python demo/apply.py
82
+ ```
83
+
84
+ The script loads the local artifact, captures GPT-2 activations from the exact
85
+ base-model revision recorded in its manifest, and prints the largest signed ICA
86
+ component scores at each token.
87
+
88
+ Use custom text or another saved artifact with:
89
+
90
+ ```bash
91
+ uv run python demo/apply.py \
92
+ --lens demo/output/icalens-gpt2-small-1k \
93
+ --layer 6 \
94
+ --text "The boat reached the bank before sunset."
95
+ ```
@@ -0,0 +1,92 @@
1
+ """Apply a saved ICA Lens to fresh GPT-2 text."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ import torch
9
+ from gb10_load_llm import load_model_to_cuda
10
+ from transformers import AutoModelForCausalLM, AutoTokenizer
11
+
12
+ from icalens import ICALens
13
+
14
+ DEFAULT_LENS = Path(__file__).parent / "output" / "icalens-gpt2-small"
15
+ DEFAULT_TEXT = "She deposited the check at the bank before walking along the river bank."
16
+
17
+
18
+ def parse_args() -> argparse.Namespace:
19
+ parser = argparse.ArgumentParser(description=__doc__)
20
+ parser.add_argument("--lens", type=Path, default=DEFAULT_LENS)
21
+ parser.add_argument("--text", default=DEFAULT_TEXT)
22
+ parser.add_argument("--layer", type=int, default=6)
23
+ parser.add_argument("--top-k", type=int, default=5)
24
+ return parser.parse_args()
25
+
26
+
27
+ def main() -> None:
28
+ args = parse_args()
29
+ if not torch.cuda.is_available():
30
+ raise RuntimeError("This GB10 demo requires a CUDA device.")
31
+ if args.top_k <= 0:
32
+ raise ValueError("--top-k must be positive")
33
+
34
+ lens = ICALens.from_pretrained(args.lens)
35
+ if args.layer not in lens.available_layers:
36
+ raise ValueError(
37
+ f"layer {args.layer} is not in this lens; available layers: {lens.available_layers}"
38
+ )
39
+
40
+ tokenizer = AutoTokenizer.from_pretrained(
41
+ lens.base_model,
42
+ revision=lens.base_model_revision,
43
+ )
44
+ model = load_model_to_cuda(
45
+ AutoModelForCausalLM,
46
+ lens.base_model,
47
+ revision=lens.base_model_revision,
48
+ device="cuda",
49
+ dtype=torch.bfloat16,
50
+ touch="auto",
51
+ low_cpu_mem_usage=True,
52
+ )
53
+ model.eval()
54
+
55
+ encoded = tokenizer(
56
+ args.text,
57
+ return_tensors="pt",
58
+ truncation=True,
59
+ max_length=tokenizer.model_max_length,
60
+ )
61
+ input_ids = encoded["input_ids"].to("cuda")
62
+ attention_mask = encoded["attention_mask"].to("cuda")
63
+ with torch.inference_mode():
64
+ outputs = model(
65
+ input_ids=input_ids,
66
+ attention_mask=attention_mask,
67
+ output_hidden_states=True,
68
+ use_cache=False,
69
+ )
70
+ if outputs.hidden_states is None:
71
+ raise RuntimeError("GPT-2 did not return hidden states.")
72
+
73
+ activations = outputs.hidden_states[args.layer + 1][0].to(dtype=torch.float32)
74
+ scores = lens.transform(activations, layer=args.layer)
75
+ top_k = min(args.top_k, scores.shape[-1])
76
+ top_indices = torch.topk(scores.abs(), k=top_k, dim=-1).indices
77
+ tokens = tokenizer.convert_ids_to_tokens(input_ids[0])
78
+
79
+ print(f"Lens: {args.lens}")
80
+ print(f"Base model: {lens.base_model}@{lens.base_model_revision}")
81
+ print(f"Layer: {args.layer}")
82
+ print()
83
+ for position, token in enumerate(tokens):
84
+ entries = [
85
+ f"C{component.item()}={scores[position, component].item():+.3f}"
86
+ for component in top_indices[position]
87
+ ]
88
+ print(f"{position:>3} {token!r:<18} {' '.join(entries)}")
89
+
90
+
91
+ if __name__ == "__main__":
92
+ main()