From 8fd82bf7311d2ef95ef0771db4722b1510d75b67 Mon Sep 17 00:00:00 2001
From: wassname <1103714+wassname@users.noreply.github.com>
Date: Fri, 10 Jul 2026 16:31:25 +0800
Subject: [PATCH] fit: tqdm progress bar + first-prompt trace + summary; config
configures loguru on import
- Jacobian.fit wraps prompts in tqdm (jlens has no bar; safe since fit only
enumerate/len's them), logs the full first prompt (special tokens on, SHOULD
line) and a done-summary -- token-efficient-logging style, both tqdm intervals set
- config.py sets up loguru on import (compact single-char icons, routed through
tqdm.write so bars survive), so every script/notebook importing config gets it
- notebooks drop their manual logger setup and import config in cell 1
Co-Authored-By: Claudypoo <288921227+claudypoo@users.noreply.github.com>
---
config.py | 11 +++++++++++
jsteer/jacobian.py | 19 +++++++++++++++++--
nbs/persona_steering.ipynb | 2 +-
nbs/word_steering.ipynb | 4 ++--
scripts/fit.py | 5 ++++-
5 files changed, 35 insertions(+), 6 deletions(-)
diff --git a/config.py b/config.py
index 191b86c..b317218 100644
--- a/config.py
+++ b/config.py
@@ -9,6 +9,17 @@ template -- see chat_corpus for why.
from pathlib import Path
import torch
+from loguru import logger
+from tqdm.auto import tqdm
+
+# Configure loguru once, on import, so every script/notebook that imports config
+# gets the same compact format. Routed through tqdm.write so log lines don't
+# break a live progress bar (e.g. the fit bar).
+logger.remove()
+logger.add(lambda m: tqdm.write(m, end=""), colorize=True,
+ format="{level.icon} {message}", level="INFO")
+for _lvl, _icon in (("INFO", "I"), ("WARNING", "W"), ("ERROR", "E"), ("DEBUG", "D")):
+ logger.level(_lvl, icon=_icon)
ROOT = Path(__file__).resolve().parent
ART = ROOT / "artifacts"
diff --git a/jsteer/jacobian.py b/jsteer/jacobian.py
index 96c1db9..9353a72 100644
--- a/jsteer/jacobian.py
+++ b/jsteer/jacobian.py
@@ -151,10 +151,25 @@ class Jacobian:
makes the fit resumable (atomic writes)."""
lm = from_hf(model, tok, compile=compile)
source_layers = _resolve_layers(layers, lm.n_layers)
- lens = _jlens_fit(lm, prompts, source_layers=source_layers,
+
+ # Full trace of the first fit prompt as jlens sees it (special tokens on).
+ # SHOULD: a chat fit opens with the template's <|im_start|>user and ends at
+ # the assistant/ start; plain text means the template was skipped.
+ ids0 = tok(prompts[0], add_special_tokens=True).input_ids
+ logger.info(f"fit on {len(prompts)} prompts, layers={source_layers} "
+ f"(dim_batch={dim_batch}, max_seq_len={max_seq_len})")
+ logger.info(f"FIT PROMPT[0] ({len(ids0)} tok): {tok.decode(ids0)!r}")
+
+ # jlens.fit has no progress bar; wrap prompts so we get one (fit consumes
+ # them only via enumerate/len, so this is safe). Both tqdm intervals set
+ # (token-efficient-logging) to avoid CR-spam in non-tty logs.
+ bar = tqdm(prompts, desc="fit J", mininterval=30, maxinterval=30)
+ lens = _jlens_fit(lm, bar, source_layers=source_layers,
dim_batch=dim_batch, max_seq_len=max_seq_len,
checkpoint_path=checkpoint_path)
- return cls(lens=lens)
+ jac = cls(lens=lens)
+ logger.info(f"fit done: {jac!r}")
+ return jac
def save(self, path: str) -> None:
self.lens.save(path) # fp16 by default; jlens-compatible file
diff --git a/nbs/persona_steering.ipynb b/nbs/persona_steering.ipynb
index 0226d02..e466a7e 100644
--- a/nbs/persona_steering.ipynb
+++ b/nbs/persona_steering.ipynb
@@ -19,7 +19,7 @@
}
},
"outputs": [],
- "source": "# demo notebook authored by Claude\nimport sys\nfrom loguru import logger\n\nlogger.remove() # show_steer prints through loguru; route it to the cell output\nlogger.add(sys.stdout, format=\"{message}\")\n\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nfrom jsteer import Jacobian, show_steer\n\nsys.path.insert(0, \"..\") # repo root for config.py\nimport config\n\nMODEL = \"Qwen/Qwen3.5-4B\" # 4B-class: demo material. 0.6B degenerates too easily.\ntok = AutoTokenizer.from_pretrained(MODEL)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()\n\n# fit-or-load the cache for THIS model. chat_corpus wraps jlens's WikiText in the\n# chat template (closer to the distribution we steer in than raw documents); the\n# lambda means WikiText is only built on a cache MISS. dim_batch=4 fits 4B on a\n# 3090; checkpoint_path makes a multi-hour 4B fit resumable if it dies.\njac = Jacobian.fit_cached(model, tok, lambda: config.chat_corpus(tok, 128),\n config.cache_path(MODEL), layers=(0.3, 0.9), dim_batch=4,\n checkpoint_path=str(config.cache_path(MODEL, \"ckpt\")))\njac"
+ "source": "# demo notebook authored by Claude\nimport sys\nsys.path.insert(0, \"..\") # repo root for config.py\nimport config # configures loguru on import (compact format, tqdm-safe)\n\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nfrom jsteer import Jacobian, show_steer\n\nMODEL = \"Qwen/Qwen3.5-4B\" # 4B-class: demo material. 0.6B degenerates too easily.\ntok = AutoTokenizer.from_pretrained(MODEL)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()\n\n# fit-or-load the cache for THIS model. chat_corpus wraps jlens's WikiText in the\n# chat template (closer to the distribution we steer in than raw documents); the\n# lambda means WikiText is only built on a cache MISS. dim_batch=4 fits 4B on a\n# 3090; checkpoint_path makes a multi-hour 4B fit resumable if it dies.\njac = Jacobian.fit_cached(model, tok, lambda: config.chat_corpus(tok, 128),\n config.cache_path(MODEL), layers=(0.3, 0.9), dim_batch=4,\n checkpoint_path=str(config.cache_path(MODEL, \"ckpt\")))\njac"
},
{
"cell_type": "markdown",
diff --git a/nbs/word_steering.ipynb b/nbs/word_steering.ipynb
index 6210fc6..aa499f0 100644
--- a/nbs/word_steering.ipynb
+++ b/nbs/word_steering.ipynb
@@ -19,7 +19,7 @@
}
},
"outputs": [],
- "source": "# demo notebook authored by Claude\nimport sys\nfrom loguru import logger\n\nlogger.remove() # show_steer prints through loguru; route it to the cell output\nlogger.add(sys.stdout, format=\"{message}\")\n\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nfrom jsteer import Jacobian, show_steer\n\nMODEL = \"Qwen/Qwen3.5-4B\" # 4B-class: demo material. 0.6B degenerates too easily.\ntok = AutoTokenizer.from_pretrained(MODEL)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()"
+ "source": "# demo notebook authored by Claude\nimport sys\nsys.path.insert(0, \"..\") # repo root for config.py\nimport config # configures loguru on import (compact format, tqdm-safe)\n\nimport torch\nfrom transformers import AutoModelForCausalLM, AutoTokenizer\n\nfrom jsteer import Jacobian, show_steer\n\nMODEL = \"Qwen/Qwen3.5-4B\" # 4B-class: demo material. 0.6B degenerates too easily.\ntok = AutoTokenizer.from_pretrained(MODEL)\nmodel = AutoModelForCausalLM.from_pretrained(MODEL, dtype=torch.bfloat16).to(\"cuda\").eval()"
},
{
"cell_type": "markdown",
@@ -40,7 +40,7 @@
}
},
"outputs": [],
- "source": "# fit-or-load: builds the cache on first run for ANY model, loads it after.\n# chat_corpus wraps jlens's WikiText in the chat template, closer to the\n# distribution we steer in (chat + ) than raw documents. The lambda means\n# WikiText is only built on a cache MISS. dim_batch=4 fits a 4B on a 24GB 3090;\n# checkpoint_path makes a multi-hour 4B fit resumable if it dies.\nsys.path.insert(0, \"..\") # repo root for config.py\nimport config\n\njac = Jacobian.fit_cached(model, tok, lambda: config.chat_corpus(tok, 128),\n config.cache_path(MODEL), layers=(0.3, 0.9), dim_batch=4,\n checkpoint_path=str(config.cache_path(MODEL, \"ckpt\")))\njac"
+ "source": "# fit-or-load: builds the cache on first run for ANY model, loads it after.\n# chat_corpus wraps jlens's WikiText in the chat template, closer to the\n# distribution we steer in (chat + ) than raw documents. The lambda means\n# WikiText is only built on a cache MISS. dim_batch=4 fits a 4B on a 24GB 3090;\n# checkpoint_path makes a multi-hour 4B fit resumable if it dies.\njac = Jacobian.fit_cached(model, tok, lambda: config.chat_corpus(tok, 128),\n config.cache_path(MODEL), layers=(0.3, 0.9), dim_batch=4,\n checkpoint_path=str(config.cache_path(MODEL, \"ckpt\")))\njac"
},
{
"cell_type": "markdown",
diff --git a/scripts/fit.py b/scripts/fit.py
index 34865f4..5aaba89 100644
--- a/scripts/fit.py
+++ b/scripts/fit.py
@@ -49,7 +49,10 @@ def main() -> None:
layers=tuple(args.layers), dim_batch=args.dim_batch,
max_seq_len=args.max_seq_len,
checkpoint_path=str(config.cache_path(args.model, "ckpt")))
- logger.info(f"{jac!r} -> {out} ({(time.monotonic() - t0) / 60:.1f} min)")
+ # BLUF summary: what to read first.
+ logger.info(f"DONE fit -> {out}")
+ logger.info(f" {jac!r} | {args.n_prompts} prompts, dim_batch={args.dim_batch}, "
+ f"{(time.monotonic() - t0) / 60:.1f} min")
if __name__ == "__main__":