Skip to content

Commit 4f2bdbe

Browse files
Merge branch 'main' of github.com:ndif-team/nnsight into main
2 parents 7a404bd + 79e1848 commit 4f2bdbe

File tree

5 files changed

+140
-70
lines changed

5 files changed

+140
-70
lines changed

README.md

Lines changed: 59 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,70 @@ Originally developed in the [NDIF team](https://ndif.us/) at Northeastern Univer
3030

3131
> 📖 For a deeper technical understanding of nnsight's internals (tracing, interleaving, the Envoy system, etc.), see **[NNsight.md](./NNsight.md)**.
3232
33+
---
34+
3335
## Installation
3436

3537
```bash
3638
pip install nnsight
3739
```
3840

41+
---
42+
43+
## Agents
44+
45+
Inform LLM agents how to use nnsight using one of these methods:
46+
47+
### Skills Repository
48+
49+
**Claude Code**
50+
51+
```bash
52+
# Open Claude Code terminal
53+
claude
54+
55+
# Add the marketplace (one time)
56+
/plugin marketplace add https://github.com/ndif-team/skills.git
57+
58+
# Install all skills
59+
/plugin install nnsight@skills
60+
```
61+
62+
**OpenAI Codex**
63+
64+
```bash
65+
# Open OpenAI Codex terminal
66+
codex
67+
68+
# Install skills
69+
skill-installer install https://github.com/ndif-team/skills.git
70+
```
71+
72+
### Context7 MCP
73+
74+
Alternatively, use [Context7](https://github.com/upstash/context7) to provide up-to-date nnsight documentation directly to your LLM. Add `use context7` to your prompts or configure it in your MCP client:
75+
76+
```json
77+
{
78+
"mcpServers": {
79+
"context7": {
80+
"url": "https://mcp.context7.com/mcp"
81+
}
82+
}
83+
}
84+
```
85+
86+
See the [Context7 README](https://github.com/upstash/context7/blob/master/README.md) for full installation instructions across different IDEs.
87+
88+
### Documentation Files
89+
90+
You can also add our documentation files directly to your agent's context:
91+
92+
- **[llms.md](./llms.md)** — Comprehensive guide for AI agents working with nnsight
93+
- **[NNsight.md](./NNsight.md)** — Deep technical documentation on nnsight's internals
94+
95+
---
96+
3997
## Quick Start
4098

4199
```python
@@ -58,8 +116,6 @@ print(model.tokenizer.decode(output.logits.argmax(dim=-1)[0]))
58116

59117
> **💡 Tip:** Always call `.save()` on values you want to access after the trace exits. Without `.save()`, values are garbage collected. You can also use `nnsight.save(value)` as an alternative.
60118
61-
---
62-
63119
## Accessing Activations
64120

65121
```python
@@ -79,8 +135,6 @@ with model.trace("The Eiffel Tower is in the city of"):
79135

80136
**Note:** GPT-2 transformer layers return tuples where index 0 contains the hidden states.
81137

82-
---
83-
84138
## Modifying Activations
85139

86140
### In-Place Modification
@@ -106,8 +160,6 @@ with model.trace("Hello"):
106160
result = model.transformer.h[-1].mlp.output.save()
107161
```
108162

109-
---
110-
111163
## Batching with Invokers
112164

113165
Process multiple inputs in one forward pass. Each invoke runs its code in a **separate worker thread**:
@@ -148,7 +200,6 @@ with model.trace() as tracer:
148200
out_all = model.lm_head.output[:, -1].save() # Shape: [3, vocab]
149201
```
150202

151-
---
152203

153204
## Multi-Token Generation
154205

@@ -198,7 +249,6 @@ with model.generate("Hello", max_new_tokens=5) as tracer:
198249
> final = model.output.save() # Now works!
199250
> ```
200251
201-
---
202252
203253
## Gradients
204254
@@ -218,7 +268,6 @@ with model.trace("Hello"):
218268
print(grad.shape)
219269
```
220270
221-
---
222271

223272
## Model Editing
224273

@@ -241,7 +290,6 @@ assert not torch.all(out1 == 0)
241290
assert torch.all(out2 == 0)
242291
```
243292

244-
---
245293

246294
## Scanning (Shape Inference)
247295

@@ -254,7 +302,6 @@ with model.scan("Hello"):
254302
print(dim) # 768
255303
```
256304

257-
---
258305

259306
## Caching Activations
260307

@@ -269,7 +316,6 @@ layer0_out = cache['model.transformer.h.0'].output
269316
print(cache.model.transformer.h[0].output[0].shape)
270317
```
271318

272-
---
273319

274320
## Sessions
275321

@@ -285,7 +331,6 @@ with model.session() as session:
285331
hs2 = model.transformer.h[0].output[0].save()
286332
```
287333

288-
---
289334

290335
## Remote Execution (NDIF)
291336

@@ -303,7 +348,6 @@ with model.trace("Hello", remote=True):
303348

304349
Check available models at [nnsight.net/status](https://nnsight.net/status/)
305350

306-
---
307351

308352
## vLLM Integration
309353

@@ -321,7 +365,6 @@ with model.trace("Hello", temperature=0.0, max_tokens=5) as tracer:
321365
logits.append(model.logits.output)
322366
```
323367

324-
---
325368

326369
## NNsight for Any PyTorch Model
327370

@@ -343,8 +386,6 @@ with model.trace(torch.rand(1, 5)):
343386
output = model.output.save()
344387
```
345388

346-
---
347-
348389
## Source Tracing
349390

350391
Access intermediate operations inside a module's forward pass. `.source` rewrites the forward method to hook into all operations:
@@ -361,8 +402,6 @@ with model.trace("Hello"):
361402
attn_out = model.transformer.h[0].attn.source.attention_interface_0.output.save()
362403
```
363404

364-
---
365-
366405
## Ad-hoc Module Application
367406

368407
Apply modules out of their normal execution order:
@@ -459,7 +498,7 @@ For more debugging tips, see the [documentation](https://www.nnsight.net).
459498

460499
- **[Documentation](https://www.nnsight.net)** — Tutorials, guides, and API reference
461500
- **[NNsight.md](./NNsight.md)** — Deep technical documentation on nnsight
462-
- **[CLAUDE.md](./CLAUDE.md)** — Comprehensive guide for AI agents working with nnsight
501+
- **[llms.md](./llms.md)** — Comprehensive guide for AI agents working with nnsight
463502

464503
---
465504

CLAUDE.md renamed to llms.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# CLAUDE.md - NNsight AI Agent Guide
1+
# llms.md - NNsight AI Agent Guide
22

33
This document provides comprehensive guidance for AI agents working with the `nnsight` library. NNsight enables interpreting and manipulating the internals states of deep learning models through a deferred execution tracing system.
44

src/nnsight/intervention/backends/remote.py

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ class Icons:
7171
def __init__(self, enabled: bool = True, verbose: bool = False):
7272
self.enabled = enabled
7373
self.verbose = verbose
74+
self.job_start_time: Optional[float] = None
7475
self.status_start_time: Optional[float] = None
7576
self.spinner_idx = 0
7677
self.last_response: Optional[Tuple[str, str, str]] = (
@@ -79,11 +80,11 @@ def __init__(self, enabled: bool = True, verbose: bool = False):
7980
self._line_written = False
8081
self._display_handle = None
8182

82-
def _format_elapsed(self) -> str:
83-
"""Format elapsed time in current status."""
84-
if self.status_start_time is None:
83+
def _format_time(self, start_time: Optional[float]) -> str:
84+
"""Format elapsed time from a given start time."""
85+
if start_time is None:
8586
return "0.0s"
86-
elapsed = time.time() - self.status_start_time
87+
elapsed = time.time() - start_time
8788
if elapsed < 60:
8889
return f"{elapsed:.1f}s"
8990
elif elapsed < 3600:
@@ -95,6 +96,14 @@ def _format_elapsed(self) -> str:
9596
mins = int((elapsed % 3600) // 60)
9697
return f"{hours}h {mins}m"
9798

99+
def _format_elapsed(self) -> str:
100+
"""Format elapsed time in current status."""
101+
return self._format_time(self.status_start_time)
102+
103+
def _format_total(self) -> str:
104+
"""Format total elapsed time since job started."""
105+
return self._format_time(self.job_start_time)
106+
98107
def _get_status_style(self, status_name: str) -> tuple:
99108
"""Get icon and color for a status."""
100109
status_map = {
@@ -128,55 +137,77 @@ def update(self, job_id: str = "", status_name: str = "", description: str = "")
128137
if not job_id:
129138
return
130139

140+
is_log = status_name == "LOG"
141+
131142
last_status = self.last_response[1] if self.last_response else None
132-
status_changed = status_name != last_status
143+
# LOG status should not be considered a status change for timer purposes
144+
status_changed = status_name != last_status and not is_log
145+
146+
# Track job start time (first status received)
147+
if last_status is None:
148+
self.job_start_time = time.time()
133149

134-
# Reset timer when status changes
150+
# Reset status timer when status changes (but not for LOG)
135151
if status_changed:
136152
self.status_start_time = time.time()
137153

138-
# Store the response
139-
self.last_response = (job_id, status_name, description)
154+
# Store the response (but not for LOG - so we go back to previous status on refresh)
155+
if not is_log:
156+
self.last_response = (job_id, status_name, description)
140157

141158
icon, color = self._get_status_style(status_name)
142-
elapsed = self._format_elapsed()
143159

144160
# Build the status line
145161
# Format: ● STATUS (elapsed) [job_id] description
146162

147163
is_terminal = status_name in ("COMPLETED", "ERROR", "NNSIGHT_ERROR")
148164
is_active = status_name in ("QUEUED", "RUNNING", "DISPATCHED")
149165

166+
# For terminal states, show total time; for others, show status elapsed time
167+
elapsed = self._format_total() if is_terminal else self._format_elapsed()
168+
150169
# For active states, show spinner
151170
if is_active:
152171
prefix = f"{self.Colors.DIM}{self._get_spinner()}{self.Colors.RESET}"
153172
else:
154173
prefix = f"{color}{icon}{self.Colors.RESET}"
155174

156175
# Build status text - full job ID shown so users can reference it
157-
status_text = (
158-
f"{prefix} "
159-
f"{self.Colors.DIM}[{job_id}]{self.Colors.RESET} "
160-
f"{color}{self.Colors.BOLD}{status_name.ljust(10)}{self.Colors.RESET} "
161-
f"{self.Colors.DIM}({elapsed}){self.Colors.RESET}"
162-
)
176+
# LOG status does not show elapsed time
177+
if is_log:
178+
status_text = (
179+
f"{prefix} "
180+
f"{self.Colors.DIM}[{job_id}]{self.Colors.RESET} "
181+
f"{color}{self.Colors.BOLD}{status_name.ljust(10)}{self.Colors.RESET}"
182+
)
183+
else:
184+
status_text = (
185+
f"{prefix} "
186+
f"{self.Colors.DIM}[{job_id}]{self.Colors.RESET} "
187+
f"{color}{self.Colors.BOLD}{status_name.ljust(10)}{self.Colors.RESET} "
188+
f"{self.Colors.DIM}({elapsed}){self.Colors.RESET}"
189+
)
163190

164191
if description:
165192
status_text += f" {self.Colors.DIM}{description}{self.Colors.RESET}"
166193

167194
# Display the status
168-
self._display(status_text, status_changed, is_terminal)
195+
# LOG status should print a newline so it's not cleared
196+
print_newline = is_terminal or is_log
197+
self._display(status_text, status_changed, print_newline)
169198

170199
self._line_written = True
171200

172-
def _display(self, text: str, status_changed: bool, is_terminal: bool):
201+
def _display(self, text: str, status_changed: bool, print_newline: bool = False):
173202
"""Display text, handling terminal vs notebook environments."""
174203
if __IPYTHON__:
175-
self._display_notebook(text, status_changed, is_terminal)
204+
self._display_notebook(text, status_changed, print_newline)
176205
else:
177-
self._display_terminal(text, status_changed, is_terminal)
206+
self._display_terminal(text, status_changed, print_newline)
178207

179-
def _display_terminal(self, text: str, status_changed: bool, is_terminal: bool):
208+
def _display_terminal(
209+
self, text: str, status_changed: bool, print_newline: bool = False
210+
):
180211
"""Display in terminal with in-place updates."""
181212
# In verbose mode, print new line when status changes
182213
if self.verbose and status_changed and self._line_written:
@@ -187,7 +218,7 @@ def _display_terminal(self, text: str, status_changed: bool, is_terminal: bool):
187218

188219
sys.stdout.write(text)
189220

190-
if is_terminal:
221+
if print_newline:
191222
sys.stdout.write("\n")
192223

193224
sys.stdout.flush()
@@ -245,7 +276,9 @@ def _ansi_to_html(self, text: str) -> str:
245276
result.append("</span>" * open_spans)
246277
return "".join(result)
247278

248-
def _display_notebook(self, text: str, status_changed: bool, is_terminal: bool):
279+
def _display_notebook(
280+
self, text: str, status_changed: bool, print_newline: bool = False
281+
):
249282
"""Display in notebook using DisplayHandle for flicker-free updates."""
250283
from IPython.display import display, HTML
251284

@@ -260,14 +293,14 @@ def _display_notebook(self, text: str, status_changed: bool, is_terminal: bool):
260293
elif self._display_handle is None:
261294
# First display
262295
self._display_handle = display(html_content, display_id=True)
296+
elif print_newline:
297+
# LOG status: create new display so it persists, then reset handle for next status
298+
display(html_content)
299+
self._display_handle = None
263300
else:
264301
# Update existing display in place (no flicker)
265302
self._display_handle.update(html_content)
266303

267-
if is_terminal:
268-
# Reset for next job
269-
self._display_handle = None
270-
271304

272305
class RemoteException(Exception):
273306
pass
@@ -376,9 +409,7 @@ def handle_response(
376409
self.job_status = response.status
377410

378411
if response.status == ResponseModel.JobStatus.ERROR:
379-
self.status_display.update(
380-
response.id, response.status.name, response.description or ""
381-
)
412+
self.status_display.update(response.id, response.status.name, "")
382413
raise RemoteException(f"{response.description}\nRemote exception.")
383414

384415
# Log response for user (skip STREAM status - it's internal)

src/nnsight/intervention/envoy.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ def device(self) -> Optional[torch.device]:
673673
except:
674674
return None
675675

676-
property
676+
@property
677677
def devices(self) -> Optional[set[torch.device]]:
678678
"""
679679
Get the devices the module is on. Finds all parameters and return their devices.

0 commit comments

Comments
 (0)