Skip to content

Commit 79e1848

Browse files
Merge pull request #592 from ndif-team/dev
refactor(config): streamline environment variable handling in AppConfigModel - Simplified the logic for setting API key and host from environment variables and Colab userdata.
2 parents 7ab8470 + eef1026 commit 79e1848

File tree

5 files changed

+145
-71
lines changed

5 files changed

+145
-71
lines changed

README.md

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,11 @@ Interpret and manipulate the internals of deep learning models
77
</h3>
88

99
<p align="center">
10-
| <a href="https://www.nnsight.net"><b>Documentation</b></a> | <a href="https://github.com/ndif-team/nnsight"><b>GitHub</b></a> | <a href="https://discord.gg/6uFJmCSwW7"><b>Discord</b></a> | <a href="https://discuss.ndif.us/"><b>Forum</b></a> | <a href="https://x.com/ndif_team"><b>Twitter</b></a> | <a href="https://arxiv.org/abs/2407.14561"><b>Paper</b></a> |
10+
<a href="https://www.nnsight.net"><b>Documentation</b></a> | <a href="https://github.com/ndif-team/nnsight"><b>GitHub</b></a> | <a href="https://discord.gg/6uFJmCSwW7"><b>Discord</b></a> | <a href="https://discuss.ndif.us/"><b>Forum</b></a> | <a href="https://x.com/ndif_team"><b>Twitter</b></a> | <a href="https://arxiv.org/abs/2407.14561"><b>Paper</b></a>
11+
</p>
12+
13+
<p align="center">
14+
<a href="https://colab.research.google.com/github/ndif-team/nnsight/blob/main/NNsight_Walkthrough.ipynb"><img src="https://colab.research.google.com/assets/colab-badge.svg"></img></a>
1115
</p>
1216

1317
---
@@ -25,12 +29,70 @@ Originally developed in the [NDIF team](https://ndif.us/) at Northeastern Univer
2529

2630
> 📖 For a deeper technical understanding of nnsight's internals (tracing, interleaving, the Envoy system, etc.), see **[NNsight.md](./NNsight.md)**.
2731
32+
---
33+
2834
## Installation
2935

3036
```bash
3137
pip install nnsight
3238
```
3339

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

3698
```python
@@ -53,8 +115,6 @@ print(model.tokenizer.decode(output.logits.argmax(dim=-1)[0]))
53115

54116
> **💡 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.
55117
56-
---
57-
58118
## Accessing Activations
59119

60120
```python
@@ -74,8 +134,6 @@ with model.trace("The Eiffel Tower is in the city of"):
74134

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

77-
---
78-
79137
## Modifying Activations
80138

81139
### In-Place Modification
@@ -101,8 +159,6 @@ with model.trace("Hello"):
101159
result = model.transformer.h[-1].mlp.output.save()
102160
```
103161

104-
---
105-
106162
## Batching with Invokers
107163

108164
Process multiple inputs in one forward pass. Each invoke runs its code in a **separate worker thread**:
@@ -143,7 +199,6 @@ with model.trace() as tracer:
143199
out_all = model.lm_head.output[:, -1].save() # Shape: [3, vocab]
144200
```
145201

146-
---
147202

148203
## Multi-Token Generation
149204

@@ -193,7 +248,6 @@ with model.generate("Hello", max_new_tokens=5) as tracer:
193248
> final = model.output.save() # Now works!
194249
> ```
195250
196-
---
197251
198252
## Gradients
199253
@@ -213,7 +267,6 @@ with model.trace("Hello"):
213267
print(grad.shape)
214268
```
215269
216-
---
217270

218271
## Model Editing
219272

@@ -236,7 +289,6 @@ assert not torch.all(out1 == 0)
236289
assert torch.all(out2 == 0)
237290
```
238291

239-
---
240292

241293
## Scanning (Shape Inference)
242294

@@ -249,7 +301,6 @@ with model.scan("Hello"):
249301
print(dim) # 768
250302
```
251303

252-
---
253304

254305
## Caching Activations
255306

@@ -264,7 +315,6 @@ layer0_out = cache['model.transformer.h.0'].output
264315
print(cache.model.transformer.h[0].output[0].shape)
265316
```
266317

267-
---
268318

269319
## Sessions
270320

@@ -280,7 +330,6 @@ with model.session() as session:
280330
hs2 = model.transformer.h[0].output[0].save()
281331
```
282332

283-
---
284333

285334
## Remote Execution (NDIF)
286335

@@ -298,7 +347,6 @@ with model.trace("Hello", remote=True):
298347

299348
Check available models at [nnsight.net/status](https://nnsight.net/status/)
300349

301-
---
302350

303351
## vLLM Integration
304352

@@ -316,7 +364,6 @@ with model.trace("Hello", temperature=0.0, max_tokens=5) as tracer:
316364
logits.append(model.logits.output)
317365
```
318366

319-
---
320367

321368
## NNsight for Any PyTorch Model
322369

@@ -338,8 +385,6 @@ with model.trace(torch.rand(1, 5)):
338385
output = model.output.save()
339386
```
340387

341-
---
342-
343388
## Source Tracing
344389

345390
Access intermediate operations inside a module's forward pass. `.source` rewrites the forward method to hook into all operations:
@@ -356,8 +401,6 @@ with model.trace("Hello"):
356401
attn_out = model.transformer.h[0].attn.source.attention_interface_0.output.save()
357402
```
358403

359-
---
360-
361404
## Ad-hoc Module Application
362405

363406
Apply modules out of their normal execution order:
@@ -454,7 +497,7 @@ For more debugging tips, see the [documentation](https://www.nnsight.net).
454497

455498
- **[Documentation](https://www.nnsight.net)** — Tutorials, guides, and API reference
456499
- **[NNsight.md](./NNsight.md)** — Deep technical documentation on nnsight
457-
- **[CLAUDE.md](./CLAUDE.md)** — Comprehensive guide for AI agents working with nnsight
500+
- **[llms.md](./llms.md)** — Comprehensive guide for AI agents working with nnsight
458501

459502
---
460503

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)

0 commit comments

Comments
 (0)