glasstrace 0.2.0__py3-none-any.whl
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.
- glasstrace/__init__.py +7 -0
- glasstrace/hooks.py +190 -0
- glasstrace/profiler.py +61 -0
- glasstrace/report.py +133 -0
- glasstrace-0.2.0.dist-info/METADATA +77 -0
- glasstrace-0.2.0.dist-info/RECORD +8 -0
- glasstrace-0.2.0.dist-info/WHEEL +4 -0
- glasstrace-0.2.0.dist-info/licenses/LICENSE +21 -0
glasstrace/__init__.py
ADDED
glasstrace/hooks.py
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Forward hooks that record per-module timing and shape info during inference."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import torch
|
|
11
|
+
import torch.nn as nn
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Phase(str, Enum):
|
|
15
|
+
PREFILL = "prefill"
|
|
16
|
+
DECODE = "decode"
|
|
17
|
+
UNKNOWN = "unknown"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ModuleEvent:
|
|
22
|
+
"""A single recorded forward pass through one module."""
|
|
23
|
+
|
|
24
|
+
module_path: str # e.g. "model.layers.0.self_attn.q_proj"
|
|
25
|
+
module_type: str # e.g. "Linear"
|
|
26
|
+
input_shape: tuple | None # shape of the first tensor input, if any
|
|
27
|
+
output_shape: tuple | None # shape of the output tensor, if any
|
|
28
|
+
duration_ms: float # how long the forward pass took, in milliseconds
|
|
29
|
+
device: str # "cuda", "mps", or "cpu"
|
|
30
|
+
phase: Phase = Phase.UNKNOWN
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class ModuleTracer:
|
|
35
|
+
"""Registers forward hooks on a model and collects per-module timing.
|
|
36
|
+
|
|
37
|
+
Uses CUDA events for accurate GPU timing when available, falls back to
|
|
38
|
+
wall-clock time otherwise. CPU/MPS wall-clock timing is approximate but
|
|
39
|
+
fine for development."""
|
|
40
|
+
|
|
41
|
+
target_types: tuple[type, ...] = (nn.Linear, nn.LayerNorm)
|
|
42
|
+
events: list[ModuleEvent] = field(default_factory=list)
|
|
43
|
+
memory_samples: list[dict] = field(default_factory=list)
|
|
44
|
+
_handles: list[Any] = field(default_factory=list)
|
|
45
|
+
_pending: dict[int, dict[str, Any]] = field(default_factory=dict)
|
|
46
|
+
_pass_count: int = 0 #tracks forward pass number
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def attach(self, model: nn.Module) -> None:
|
|
50
|
+
"""Walk the model and register hooks on every module of a target type."""
|
|
51
|
+
for name, module in model.named_modules():
|
|
52
|
+
if isinstance(module, self.target_types):
|
|
53
|
+
pre_handle = module.register_forward_pre_hook(
|
|
54
|
+
self._make_pre_hook(name, type(module).__name__)
|
|
55
|
+
)
|
|
56
|
+
post_handle = module.register_forward_hook(
|
|
57
|
+
self._make_post_hook(name, type(module).__name__)
|
|
58
|
+
)
|
|
59
|
+
self._handles.extend([pre_handle, post_handle])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
self._attach_memory_sampler(model)
|
|
63
|
+
|
|
64
|
+
def _attach_memory_sampler(self, model: nn.Module) -> None:
|
|
65
|
+
"""Sample GPU memory allocated at the start of each forward pass."""
|
|
66
|
+
import torch
|
|
67
|
+
if not torch.cuda.is_available():
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
tracer_ref = self # capture self for the closure
|
|
71
|
+
|
|
72
|
+
for name, module in model.named_modules():
|
|
73
|
+
if isinstance(module, nn.Linear):
|
|
74
|
+
def memory_hook(mod, inputs):
|
|
75
|
+
mem_bytes = torch.cuda.memory_allocated()
|
|
76
|
+
phase = tracer_ref._detect_phase(
|
|
77
|
+
tracer_ref._shape_of(inputs[0]) if inputs else None
|
|
78
|
+
)
|
|
79
|
+
tracer_ref.memory_samples.append({
|
|
80
|
+
"pass": tracer_ref._pass_count,
|
|
81
|
+
"phase": phase.value,
|
|
82
|
+
"memory_bytes": mem_bytes,
|
|
83
|
+
})
|
|
84
|
+
tracer_ref._pass_count += 1
|
|
85
|
+
|
|
86
|
+
handle = module.register_forward_pre_hook(memory_hook)
|
|
87
|
+
self._handles.append(handle)
|
|
88
|
+
break # first Linear only
|
|
89
|
+
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
def detach(self) -> None:
|
|
93
|
+
"""Remove all registered hooks."""
|
|
94
|
+
for handle in self._handles:
|
|
95
|
+
handle.remove()
|
|
96
|
+
self._handles.clear()
|
|
97
|
+
self._pending.clear()
|
|
98
|
+
|
|
99
|
+
def _make_pre_hook(self, module_path: str, module_type: str):
|
|
100
|
+
def pre_hook(module: nn.Module, inputs: tuple) -> None:
|
|
101
|
+
device = self._device_of(inputs, module)
|
|
102
|
+
input_shape = self._shape_of(inputs[0]) if inputs else None
|
|
103
|
+
|
|
104
|
+
timing: dict[str, Any] = {
|
|
105
|
+
"module_path": module_path,
|
|
106
|
+
"module_type": module_type,
|
|
107
|
+
"input_shape": input_shape,
|
|
108
|
+
"device": device,
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if device == "cuda":
|
|
112
|
+
start = torch.cuda.Event(enable_timing=True)
|
|
113
|
+
end = torch.cuda.Event(enable_timing=True)
|
|
114
|
+
start.record()
|
|
115
|
+
timing["cuda_start"] = start
|
|
116
|
+
timing["cuda_end"] = end
|
|
117
|
+
else:
|
|
118
|
+
timing["wall_start"] = time.perf_counter()
|
|
119
|
+
|
|
120
|
+
self._pending[id(module)] = timing
|
|
121
|
+
|
|
122
|
+
return pre_hook
|
|
123
|
+
|
|
124
|
+
def _make_post_hook(self, module_path: str, module_type: str):
|
|
125
|
+
def post_hook(module: nn.Module, inputs: tuple, output: Any) -> None:
|
|
126
|
+
timing = self._pending.pop(id(module), None)
|
|
127
|
+
if timing is None:
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
output_shape = self._shape_of(output)
|
|
131
|
+
|
|
132
|
+
if timing["device"] == "cuda":
|
|
133
|
+
timing["cuda_end"].record()
|
|
134
|
+
torch.cuda.synchronize()
|
|
135
|
+
duration_ms = timing["cuda_start"].elapsed_time(timing["cuda_end"])
|
|
136
|
+
else:
|
|
137
|
+
duration_ms = (time.perf_counter() - timing["wall_start"]) * 1000.0
|
|
138
|
+
|
|
139
|
+
# Detect phase from input sequence dimension
|
|
140
|
+
phase = self._detect_phase(timing["input_shape"])
|
|
141
|
+
|
|
142
|
+
self.events.append(
|
|
143
|
+
ModuleEvent(
|
|
144
|
+
module_path=timing["module_path"],
|
|
145
|
+
module_type=timing["module_type"],
|
|
146
|
+
input_shape=timing["input_shape"],
|
|
147
|
+
output_shape=output_shape,
|
|
148
|
+
duration_ms=duration_ms,
|
|
149
|
+
device=timing["device"],
|
|
150
|
+
phase=phase,
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return post_hook
|
|
155
|
+
|
|
156
|
+
@staticmethod
|
|
157
|
+
def _shape_of(x: Any) -> tuple | None:
|
|
158
|
+
if isinstance(x, torch.Tensor):
|
|
159
|
+
return tuple(x.shape)
|
|
160
|
+
if isinstance(x, (list, tuple)) and len(x) > 0 and isinstance(x[0], torch.Tensor):
|
|
161
|
+
return tuple(x[0].shape)
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
@staticmethod
|
|
165
|
+
def _device_of(inputs: tuple, module: nn.Module) -> str:
|
|
166
|
+
# Prefer the input's device; fall back to a parameter's device.
|
|
167
|
+
if inputs and isinstance(inputs[0], torch.Tensor):
|
|
168
|
+
return inputs[0].device.type
|
|
169
|
+
for p in module.parameters():
|
|
170
|
+
return p.device.type
|
|
171
|
+
return "cpu"
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _detect_phase(input_shape: tuple | None) -> Phase:
|
|
176
|
+
"""Infer prefill vs decode from the sequence dimension of the input.
|
|
177
|
+
|
|
178
|
+
For decoder-only transformers: seq_len > 1 means prefill (processing
|
|
179
|
+
the full prompt). seq_len == 1 means decode (one new token per pass).
|
|
180
|
+
"""
|
|
181
|
+
if input_shape is None:
|
|
182
|
+
return Phase.UNKNOWN
|
|
183
|
+
# Shape is (batch, seq_len, hidden_dim) for most transformer layers
|
|
184
|
+
if len(input_shape) >= 2:
|
|
185
|
+
seq_len = input_shape[1]
|
|
186
|
+
if seq_len == 1:
|
|
187
|
+
return Phase.DECODE
|
|
188
|
+
if seq_len > 1:
|
|
189
|
+
return Phase.PREFILL
|
|
190
|
+
return Phase.UNKNOWN
|
glasstrace/profiler.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""The user-facing profile() context manager."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Callable
|
|
8
|
+
|
|
9
|
+
import torch.nn as nn
|
|
10
|
+
|
|
11
|
+
from glasstrace.hooks import ModuleEvent, ModuleTracer
|
|
12
|
+
from glasstrace.report import format_report
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ProfileResult:
|
|
17
|
+
"""Holds events and memory samples from a profile() block."""
|
|
18
|
+
|
|
19
|
+
events: list[ModuleEvent] = field(default_factory=list)
|
|
20
|
+
memory_samples: list[dict] = field(default_factory=list)
|
|
21
|
+
|
|
22
|
+
def report(self, top_n: int = 20) -> str:
|
|
23
|
+
"""Return a formatted two-section text report."""
|
|
24
|
+
return format_report(self.events, self.memory_samples, top_n=top_n)
|
|
25
|
+
|
|
26
|
+
def __len__(self) -> int:
|
|
27
|
+
return len(self.events)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@contextmanager
|
|
31
|
+
def profile(model: nn.Module, warmup: Callable[[], None] | None = None):
|
|
32
|
+
"""Profile a model's forward passes within a with-block.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
model: the model to instrument.
|
|
36
|
+
warmup: optional zero-arg callable run once before profiling starts,
|
|
37
|
+
with its events discarded. Strongly recommended on CUDA to avoid
|
|
38
|
+
cold-start timing artifacts.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
def warmup():
|
|
42
|
+
model.generate(**inputs, max_new_tokens=5)
|
|
43
|
+
|
|
44
|
+
with glasstrace.profile(model, warmup=warmup) as p:
|
|
45
|
+
model.generate(**inputs, max_new_tokens=50)
|
|
46
|
+
print(p.report())
|
|
47
|
+
"""
|
|
48
|
+
if warmup is not None:
|
|
49
|
+
import torch
|
|
50
|
+
with torch.no_grad():
|
|
51
|
+
warmup()
|
|
52
|
+
if torch.cuda.is_available():
|
|
53
|
+
torch.cuda.synchronize()
|
|
54
|
+
|
|
55
|
+
tracer = ModuleTracer()
|
|
56
|
+
tracer.attach(model)
|
|
57
|
+
result = ProfileResult(events=tracer.events, memory_samples=tracer.memory_samples)
|
|
58
|
+
try:
|
|
59
|
+
yield result
|
|
60
|
+
finally:
|
|
61
|
+
tracer.detach()
|
glasstrace/report.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Text-table report generation from ModuleEvent lists."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from tabulate import tabulate
|
|
9
|
+
|
|
10
|
+
from glasstrace.hooks import ModuleEvent, Phase
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _aggregate(events: list[ModuleEvent]) -> list[dict]:
|
|
14
|
+
"""Aggregate events by module path: sum times, count calls."""
|
|
15
|
+
agg: dict[str, dict] = defaultdict(
|
|
16
|
+
lambda: {"calls": 0, "total_ms": 0.0, "module_type": "", "device": ""}
|
|
17
|
+
)
|
|
18
|
+
for e in events:
|
|
19
|
+
a = agg[e.module_path]
|
|
20
|
+
a["calls"] += 1
|
|
21
|
+
a["total_ms"] += e.duration_ms
|
|
22
|
+
a["module_type"] = e.module_type
|
|
23
|
+
a["device"] = e.device
|
|
24
|
+
return [
|
|
25
|
+
{"path": path, **vals}
|
|
26
|
+
for path, vals in sorted(
|
|
27
|
+
agg.items(), key=lambda x: x[1]["total_ms"], reverse=True
|
|
28
|
+
)
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _section_table(rows: list[dict], total_ms: float, extra_col: str | None = None) -> str:
|
|
33
|
+
"""Format a list of aggregated module rows as a text table."""
|
|
34
|
+
if not rows:
|
|
35
|
+
return " (no events)\n"
|
|
36
|
+
|
|
37
|
+
table_rows = []
|
|
38
|
+
for r in rows:
|
|
39
|
+
row = {
|
|
40
|
+
"Module": r["path"],
|
|
41
|
+
"Type": r["module_type"],
|
|
42
|
+
"Calls": r["calls"],
|
|
43
|
+
"Total ms": f"{r['total_ms']:.2f}",
|
|
44
|
+
"Per-call ms": f"{r['total_ms'] / r['calls']:.2f}",
|
|
45
|
+
"% of phase": f"{r['total_ms'] / total_ms * 100:.1f}" if total_ms > 0 else "—",
|
|
46
|
+
}
|
|
47
|
+
table_rows.append(row)
|
|
48
|
+
|
|
49
|
+
return tabulate(table_rows, headers="keys", tablefmt="simple") + "\n"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def format_report(
|
|
53
|
+
events: Iterable[ModuleEvent],
|
|
54
|
+
memory_samples: list[dict] | None = None,
|
|
55
|
+
top_n: int = 20,
|
|
56
|
+
) -> str:
|
|
57
|
+
"""Produce a two-section report: prefill and decode."""
|
|
58
|
+
events = list(events)
|
|
59
|
+
if not events:
|
|
60
|
+
return (
|
|
61
|
+
"glasstrace: no events recorded.\n"
|
|
62
|
+
"(Was the model actually run inside the profile() block?)"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
device = events[0].device
|
|
66
|
+
|
|
67
|
+
prefill = [e for e in events if e.phase == Phase.PREFILL]
|
|
68
|
+
decode = [e for e in events if e.phase == Phase.DECODE]
|
|
69
|
+
unknown = [e for e in events if e.phase == Phase.UNKNOWN]
|
|
70
|
+
|
|
71
|
+
prefill_ms = sum(e.duration_ms for e in prefill)
|
|
72
|
+
decode_ms = sum(e.duration_ms for e in decode)
|
|
73
|
+
total_ms = sum(e.duration_ms for e in events)
|
|
74
|
+
|
|
75
|
+
# Decode passes = number of unique decode events for one module
|
|
76
|
+
# (all modules fire once per decode token)
|
|
77
|
+
decode_passes = decode[0].module_path and len(
|
|
78
|
+
[e for e in decode if e.module_path == decode[0].module_path]
|
|
79
|
+
) if decode else 0
|
|
80
|
+
per_token_ms = decode_ms / decode_passes if decode_passes > 0 else 0.0
|
|
81
|
+
|
|
82
|
+
# Memory summary
|
|
83
|
+
mem_summary = ""
|
|
84
|
+
if memory_samples:
|
|
85
|
+
decode_samples = [s for s in memory_samples if s["phase"] == "decode"]
|
|
86
|
+
if decode_samples:
|
|
87
|
+
min_mem = min(s["memory_bytes"] for s in decode_samples)
|
|
88
|
+
max_mem = max(s["memory_bytes"] for s in decode_samples)
|
|
89
|
+
kv_growth_mb = (max_mem - min_mem) / (1024 ** 2)
|
|
90
|
+
mem_summary = f" kv-cache growth during decode: {kv_growth_mb:.1f} MB\n"
|
|
91
|
+
|
|
92
|
+
header = (
|
|
93
|
+
f"\nglasstrace report\n"
|
|
94
|
+
f" modules profiled: {len({e.module_path for e in events})}\n"
|
|
95
|
+
f" total events: {len(events)}\n"
|
|
96
|
+
f" total measured time: {total_ms:.2f} ms\n"
|
|
97
|
+
f" device: {device}\n"
|
|
98
|
+
+ mem_summary
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Prefill section
|
|
102
|
+
prefill_header = (
|
|
103
|
+
f"\n── prefill (1 pass, {prefill_ms:.1f} ms total) "
|
|
104
|
+
+ "─" * 40 + "\n"
|
|
105
|
+
)
|
|
106
|
+
prefill_rows = _aggregate(prefill)[:top_n]
|
|
107
|
+
prefill_table = _section_table(prefill_rows, prefill_ms)
|
|
108
|
+
|
|
109
|
+
# Decode section
|
|
110
|
+
decode_header = (
|
|
111
|
+
f"\n── decode ({decode_passes} passes, {decode_ms:.1f} ms total"
|
|
112
|
+
+ (f", {per_token_ms:.1f} ms/token avg" if per_token_ms > 0 else "")
|
|
113
|
+
+ ") " + "─" * 20 + "\n"
|
|
114
|
+
)
|
|
115
|
+
decode_rows = _aggregate(decode)[:top_n]
|
|
116
|
+
decode_table = _section_table(decode_rows, decode_ms)
|
|
117
|
+
|
|
118
|
+
# Unknown section (should be empty for standard transformer runs)
|
|
119
|
+
unknown_section = ""
|
|
120
|
+
if unknown:
|
|
121
|
+
unknown_ms = sum(e.duration_ms for e in unknown)
|
|
122
|
+
unknown_section = (
|
|
123
|
+
f"\n── unclassified ({len(unknown)} events, {unknown_ms:.1f} ms) ──\n"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
header
|
|
128
|
+
+ prefill_header
|
|
129
|
+
+ prefill_table
|
|
130
|
+
+ decode_header
|
|
131
|
+
+ decode_table
|
|
132
|
+
+ unknown_section
|
|
133
|
+
)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: glasstrace
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Per-layer latency and memory profiler for transformer inference.
|
|
5
|
+
Project-URL: Homepage, https://github.com/manu-j3400/glasstrace
|
|
6
|
+
Project-URL: Repository, https://github.com/manu-j3400/glasstrace
|
|
7
|
+
Project-URL: Issues, https://github.com/manu-j3400/glasstrace/issues
|
|
8
|
+
Author-email: Manu <therealmanujawahar@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: inference,llm,profiler,pytorch,transformers
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Requires-Dist: tabulate>=0.9
|
|
21
|
+
Requires-Dist: torch>=2.0
|
|
22
|
+
Requires-Dist: transformers>=4.40
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# glasstrace
|
|
29
|
+
[](https://github.com/manu-j3400/glasstrace/actions/workflows/ci.yml)
|
|
30
|
+
|
|
31
|
+
> Per-layer latency and memory profiler for transformer inference.
|
|
32
|
+
|
|
33
|
+
`glasstrace` shows you where time actually goes inside your LLM. Decomposes inference cost by layer, operation, and inference phase (prefill vs decode).
|
|
34
|
+
|
|
35
|
+
## Why
|
|
36
|
+
|
|
37
|
+
When you call `model.generate()`, you get a number: total latency. That's not enough to make anything faster. `glasstrace` turns the black box into a measured picture: which layers are slow, where memory pressure lives, and what changes when you tweak batch size or sequence length.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pip install git+https://github.com/manu-j3400/glasstrace.git
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
PyPI release coming with v1.0.
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
import glasstrace
|
|
51
|
+
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
52
|
+
|
|
53
|
+
model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-0.5B")
|
|
54
|
+
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B")
|
|
55
|
+
inputs = tokenizer("Hello, world!", return_tensors="pt")
|
|
56
|
+
|
|
57
|
+
with glasstrace.profile(model) as p:
|
|
58
|
+
out = model.generate(**inputs, max_new_tokens=50)
|
|
59
|
+
|
|
60
|
+
print(p.report())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Status
|
|
64
|
+
|
|
65
|
+
**v0.1.0 — alpha.** Works on Qwen 2.5 0.5B and Llama 3.2 1B with CUDA. Tracks `nn.Linear` and `nn.LayerNorm` modules. Memory tracking, HTML reports, and broader model coverage planned for v0.2.
|
|
66
|
+
|
|
67
|
+
## Roadmap
|
|
68
|
+
|
|
69
|
+
- [x] v0.1 — Per-module CUDA timing, text-table report
|
|
70
|
+
- [x] v0.2 — Prefill vs decode split, memory tracking, HTML report
|
|
71
|
+
- [ ] v0.3 — Multi-model tested coverage, CLI
|
|
72
|
+
- [ ] v0.4 — Comparative analyses across Llama, Qwen, Phi (blog post)
|
|
73
|
+
- [ ] v1.0 — PyPI release, docs, demo video
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
glasstrace/__init__.py,sha256=eMu0nO65p3mgvtn3j1SKJTkDVxiA70dEnZUSDz-d7Ck,201
|
|
2
|
+
glasstrace/hooks.py,sha256=EpH-sRjpTlLUiV4IK3Vek8RhMHaRJWLMpxqYXn_DsiQ,6883
|
|
3
|
+
glasstrace/profiler.py,sha256=TtyLyGSkyNId1rfwzZGJLlco9twFhZbG11yzahqGo4I,1804
|
|
4
|
+
glasstrace/report.py,sha256=qtSVIdpMBozyCkKjfd-G9ViEpfZynljOaINHvPBDGSg,4410
|
|
5
|
+
glasstrace-0.2.0.dist-info/METADATA,sha256=PnVOhQWIBb6PJTSnfMvJyfZZYBMEP7QeUIy1ddsoBB0,2813
|
|
6
|
+
glasstrace-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
7
|
+
glasstrace-0.2.0.dist-info/licenses/LICENSE,sha256=M0ttOeZIwgeeugfuDScWfFP88rhf_MKucUHTS5V_Rvk,1069
|
|
8
|
+
glasstrace-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Manu Jawahar
|
|
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.
|