traceAI-transformers 0.1.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.
- traceai_transformers/__init__.py +79 -0
- traceai_transformers/_wrappers.py +336 -0
- traceai_transformers/version.py +1 -0
- traceai_transformers-0.1.0.dist-info/METADATA +103 -0
- traceai_transformers-0.1.0.dist-info/RECORD +7 -0
- traceai_transformers-0.1.0.dist-info/WHEEL +4 -0
- traceai_transformers-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Trace local Transformers pipelines, independently of remote Hub clients."""
|
|
2
|
+
from typing import Any, Collection
|
|
3
|
+
|
|
4
|
+
from fi_instrumentation import FITracer, TraceConfig
|
|
5
|
+
from opentelemetry import trace
|
|
6
|
+
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
|
|
7
|
+
from wrapt import FunctionWrapper
|
|
8
|
+
|
|
9
|
+
from ._wrappers import _PipelineWrapper
|
|
10
|
+
from .version import __version__
|
|
11
|
+
|
|
12
|
+
# Look up optional historical classes individually: Transformers 5 removed them.
|
|
13
|
+
_TARGETS = (
|
|
14
|
+
("TextGenerationPipeline", "text-generation", "LLM"),
|
|
15
|
+
("FeatureExtractionPipeline", "feature-extraction", "EMBEDDING"),
|
|
16
|
+
("Text2TextGenerationPipeline", "text2text-generation", "LLM"),
|
|
17
|
+
("SummarizationPipeline", "summarization", "LLM"),
|
|
18
|
+
("TranslationPipeline", "translation", "LLM"),
|
|
19
|
+
("QuestionAnsweringPipeline", "question-answering", "CHAIN"),
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TransformersInstrumentor(BaseInstrumentor):
|
|
24
|
+
"""Instrument supported pipeline calls; bare ``model.generate`` is not patched.
|
|
25
|
+
|
|
26
|
+
``capture_content=False`` (default) records metadata only. Opt in to bounded
|
|
27
|
+
input/output previews with ``capture_content=True``; ``config=TraceConfig``
|
|
28
|
+
still controls privacy. Embedding vectors and lazy result payloads are never
|
|
29
|
+
collected. Supply ``tracer_provider`` to avoid changing the global provider.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def instrumentation_dependencies(self) -> Collection[str]:
|
|
33
|
+
return ("transformers>=4.42,<6",)
|
|
34
|
+
|
|
35
|
+
def _instrument(self, **kwargs: Any) -> None:
|
|
36
|
+
import transformers.pipelines as pipelines
|
|
37
|
+
|
|
38
|
+
config = kwargs.get("config")
|
|
39
|
+
if config is None:
|
|
40
|
+
config = TraceConfig()
|
|
41
|
+
if not isinstance(config, TraceConfig):
|
|
42
|
+
raise TypeError("config must be a TraceConfig")
|
|
43
|
+
capture = kwargs.get("capture_content", False)
|
|
44
|
+
if not isinstance(capture, bool):
|
|
45
|
+
raise TypeError("capture_content must be a bool")
|
|
46
|
+
tracer = FITracer(
|
|
47
|
+
trace.get_tracer(__name__, __version__, kwargs.get("tracer_provider")),
|
|
48
|
+
config=config,
|
|
49
|
+
)
|
|
50
|
+
self._patches = []
|
|
51
|
+
try:
|
|
52
|
+
for name, task, kind in _TARGETS:
|
|
53
|
+
cls = getattr(pipelines, name, None)
|
|
54
|
+
if cls is None:
|
|
55
|
+
continue
|
|
56
|
+
own_method = "__call__" in vars(cls)
|
|
57
|
+
original = vars(cls)["__call__"] if own_method else cls.__call__
|
|
58
|
+
wrapper = _PipelineWrapper(tracer=tracer, config=config, capture=capture, task=task, kind=kind)
|
|
59
|
+
patched = FunctionWrapper(original, wrapper)
|
|
60
|
+
self._patches.append((cls, own_method, original, patched, wrapper))
|
|
61
|
+
cls.__call__ = patched
|
|
62
|
+
except BaseException:
|
|
63
|
+
self._uninstrument()
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
def _uninstrument(self, **kwargs: Any) -> None:
|
|
67
|
+
for cls, own_method, original, patched, wrapper in reversed(self._patches):
|
|
68
|
+
# An outer, third-party patch may still hold ours: disable it without
|
|
69
|
+
# overwriting their patch. In-flight lazy iterators finish normally.
|
|
70
|
+
wrapper.enabled = False
|
|
71
|
+
if vars(cls).get("__call__") is patched:
|
|
72
|
+
if own_method:
|
|
73
|
+
cls.__call__ = original
|
|
74
|
+
else:
|
|
75
|
+
delattr(cls, "__call__")
|
|
76
|
+
self._patches = []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["TransformersInstrumentor", "__version__"]
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""Bounded metadata and iteration-scoped tracing without touching model outputs."""
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterator
|
|
5
|
+
from contextlib import contextmanager
|
|
6
|
+
from contextvars import ContextVar
|
|
7
|
+
from copy import copy
|
|
8
|
+
from itertools import islice
|
|
9
|
+
from time import time_ns
|
|
10
|
+
from types import GeneratorType
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
|
|
13
|
+
from fi_instrumentation import FITracer, TraceConfig
|
|
14
|
+
from opentelemetry import context, trace
|
|
15
|
+
from opentelemetry.context import _SUPPRESS_INSTRUMENTATION_KEY
|
|
16
|
+
from opentelemetry.trace import Status, StatusCode
|
|
17
|
+
from wrapt import ObjectProxy
|
|
18
|
+
|
|
19
|
+
_ACTIVE: ContextVar[tuple[int, ...]] = ContextVar("traceai_transformers_active", default=())
|
|
20
|
+
_MAX_PREVIEW = 4096
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
logger.addHandler(logging.NullHandler())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _log_failure(action: str, error: Exception) -> None:
|
|
26
|
+
# Telemetry is fail-open, but not silent. Do not log exception messages or
|
|
27
|
+
# traceback source lines: they can bypass TraceConfig's content privacy.
|
|
28
|
+
logger.warning("Transformers instrumentation failed to %s (%s)", action, type(error).__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _record_error(span, error: BaseException, config: TraceConfig) -> None:
|
|
32
|
+
if config.pii_redaction or config.hide_error_details:
|
|
33
|
+
# Supported FI core versions mask supplied attributes, but not the
|
|
34
|
+
# message/traceback generated by record_exception or status descriptions.
|
|
35
|
+
# Omit those surfaces entirely, including chained errors and notes;
|
|
36
|
+
# never stringify or mutate the exception being returned to the caller.
|
|
37
|
+
cls = type(error)
|
|
38
|
+
exception_type = cls.__qualname__
|
|
39
|
+
if cls.__module__ and cls.__module__ != "builtins":
|
|
40
|
+
exception_type = f"{cls.__module__}.{exception_type}"
|
|
41
|
+
span.add_event("exception", {
|
|
42
|
+
"exception.type": exception_type,
|
|
43
|
+
"exception.escaped": "False",
|
|
44
|
+
})
|
|
45
|
+
span.set_status(StatusCode.ERROR)
|
|
46
|
+
else:
|
|
47
|
+
span.record_exception(error)
|
|
48
|
+
span.set_status(Status(StatusCode.ERROR, str(error)))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _preview(value: object) -> str:
|
|
52
|
+
# No repr/default=str, tensor conversion, custom iteration or full JSON
|
|
53
|
+
# serialization of unbounded collections. A global node budget also bounds
|
|
54
|
+
# nested batches, not just each individual list.
|
|
55
|
+
budget = [64]
|
|
56
|
+
|
|
57
|
+
def trim(item, depth=0):
|
|
58
|
+
budget[0] -= 1
|
|
59
|
+
if budget[0] < 0 or depth > 5:
|
|
60
|
+
return "[truncated]"
|
|
61
|
+
if type(item) is str:
|
|
62
|
+
return item[:_MAX_PREVIEW]
|
|
63
|
+
if item is None or type(item) in (bool, int, float):
|
|
64
|
+
return item
|
|
65
|
+
if type(item) in (list, tuple):
|
|
66
|
+
return [trim(v, depth + 1) for v in item[:16] if budget[0] > 0]
|
|
67
|
+
if type(item) is dict:
|
|
68
|
+
result = {}
|
|
69
|
+
for key, val in islice(item.items(), 16):
|
|
70
|
+
if budget[0] <= 0 or len(result) >= 16:
|
|
71
|
+
break
|
|
72
|
+
if type(key) is str:
|
|
73
|
+
result[key[:128]] = trim(val, depth + 1)
|
|
74
|
+
return result
|
|
75
|
+
return "[not captured]"
|
|
76
|
+
|
|
77
|
+
return json.dumps(trim(value), ensure_ascii=True)[:_MAX_PREVIEW]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _Invocation:
|
|
81
|
+
def __init__(self, wrapper, instance, value, parent):
|
|
82
|
+
self.tracer = wrapper.tracer
|
|
83
|
+
self.config = wrapper.config
|
|
84
|
+
self.identity = id(instance)
|
|
85
|
+
self.parent = parent
|
|
86
|
+
self.name = "transformers." + wrapper.task
|
|
87
|
+
self.attributes = {
|
|
88
|
+
"gen_ai.span.kind": wrapper.kind,
|
|
89
|
+
"gen_ai.provider.name": "huggingface",
|
|
90
|
+
"gen_ai.operation.name": (
|
|
91
|
+
"embeddings" if wrapper.kind == "EMBEDDING" else
|
|
92
|
+
"text_completion" if wrapper.kind == "LLM" else "question_answering"
|
|
93
|
+
),
|
|
94
|
+
"transformers.task": wrapper.task,
|
|
95
|
+
}
|
|
96
|
+
# Model architecture is safe metadata, not a local filesystem path or
|
|
97
|
+
# arbitrary user-supplied model identifier.
|
|
98
|
+
model_type = getattr(getattr(getattr(instance, "model", None), "config", None), "model_type", None)
|
|
99
|
+
if type(model_type) is str:
|
|
100
|
+
self.attributes["gen_ai.request.model"] = model_type[:256]
|
|
101
|
+
# Generation responses can contain the entire original prompt/chat.
|
|
102
|
+
# Never re-export hidden inputs through a full-text output preview.
|
|
103
|
+
self.capture_output = wrapper.capture and wrapper.kind != "EMBEDDING" and not (
|
|
104
|
+
wrapper.config.hide_outputs or wrapper.config.hide_output_text or
|
|
105
|
+
wrapper.config.hide_output_messages or wrapper.config.hide_inputs or
|
|
106
|
+
wrapper.config.hide_input_text or wrapper.config.hide_input_messages
|
|
107
|
+
)
|
|
108
|
+
if wrapper.capture and not (
|
|
109
|
+
wrapper.config.hide_inputs or wrapper.config.hide_input_text or
|
|
110
|
+
wrapper.config.hide_input_messages
|
|
111
|
+
):
|
|
112
|
+
self.attributes["input.value"] = _preview(value)
|
|
113
|
+
self.span = None
|
|
114
|
+
self.started = False
|
|
115
|
+
self.finished = False
|
|
116
|
+
|
|
117
|
+
def start(self, start_time=None):
|
|
118
|
+
if not self.started:
|
|
119
|
+
self.started = True
|
|
120
|
+
try:
|
|
121
|
+
self.span = self.tracer.start_span(
|
|
122
|
+
self.name, context=self.parent, attributes=self.attributes, start_time=start_time,
|
|
123
|
+
)
|
|
124
|
+
except Exception as error:
|
|
125
|
+
# An exporter/provider failure must not prevent local inference.
|
|
126
|
+
_log_failure("start span", error)
|
|
127
|
+
self.attributes = None
|
|
128
|
+
|
|
129
|
+
@contextmanager
|
|
130
|
+
def activate(self):
|
|
131
|
+
token = context.attach(self.parent)
|
|
132
|
+
active = _ACTIVE.set((*_ACTIVE.get(), self.identity))
|
|
133
|
+
try:
|
|
134
|
+
self.start()
|
|
135
|
+
if self.span is None:
|
|
136
|
+
yield
|
|
137
|
+
else:
|
|
138
|
+
with trace.use_span(self.span, end_on_exit=False, record_exception=False,
|
|
139
|
+
set_status_on_exception=False):
|
|
140
|
+
yield
|
|
141
|
+
finally:
|
|
142
|
+
_ACTIVE.reset(active)
|
|
143
|
+
context.detach(token)
|
|
144
|
+
|
|
145
|
+
def finish(self, error=None, output=None):
|
|
146
|
+
if self.finished:
|
|
147
|
+
return
|
|
148
|
+
self.finished = True
|
|
149
|
+
if self.span is None:
|
|
150
|
+
return
|
|
151
|
+
try:
|
|
152
|
+
if error is not None:
|
|
153
|
+
_record_error(self.span, error, self.config)
|
|
154
|
+
else:
|
|
155
|
+
if self.capture_output and output is not None:
|
|
156
|
+
self.span.set_attribute("output.value", _preview(output))
|
|
157
|
+
self.span.set_status(StatusCode.OK)
|
|
158
|
+
except Exception as telemetry_error:
|
|
159
|
+
_log_failure("record span result", telemetry_error)
|
|
160
|
+
finally:
|
|
161
|
+
try:
|
|
162
|
+
self.span.end()
|
|
163
|
+
except Exception as telemetry_error:
|
|
164
|
+
_log_failure("end span", telemetry_error)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class _TracedIterator(Iterator):
|
|
168
|
+
"""One span per lazy call, current only while the producer executes.
|
|
169
|
+
|
|
170
|
+
Payloads are never accumulated. Close an abandoned iterator explicitly;
|
|
171
|
+
finalization is only a best-effort fallback, as with Python generators.
|
|
172
|
+
"""
|
|
173
|
+
def __init__(self, wrapped, invocation, *, iterator=None):
|
|
174
|
+
self._wrapped = wrapped
|
|
175
|
+
self._iterator = iterator
|
|
176
|
+
self._invocation = invocation
|
|
177
|
+
self._closed = False
|
|
178
|
+
|
|
179
|
+
def __next__(self):
|
|
180
|
+
if self._closed:
|
|
181
|
+
raise StopIteration
|
|
182
|
+
try:
|
|
183
|
+
with self._invocation.activate():
|
|
184
|
+
if self._iterator is None:
|
|
185
|
+
self._iterator = iter(self._wrapped)
|
|
186
|
+
return next(self._iterator)
|
|
187
|
+
except StopIteration:
|
|
188
|
+
self._closed = True
|
|
189
|
+
self._invocation.finish()
|
|
190
|
+
self._wrapped = self._iterator = None
|
|
191
|
+
raise
|
|
192
|
+
except BaseException as error:
|
|
193
|
+
self._closed = True
|
|
194
|
+
self._invocation.finish(error)
|
|
195
|
+
self._wrapped = self._iterator = None
|
|
196
|
+
raise
|
|
197
|
+
|
|
198
|
+
def close(self):
|
|
199
|
+
if self._closed:
|
|
200
|
+
return
|
|
201
|
+
self._closed = True
|
|
202
|
+
try:
|
|
203
|
+
closer = getattr(self._wrapped, "close", None)
|
|
204
|
+
if closer is not None:
|
|
205
|
+
# Do not start an inference span for an unconsumed iterator.
|
|
206
|
+
if self._invocation.started:
|
|
207
|
+
with self._invocation.activate():
|
|
208
|
+
closer()
|
|
209
|
+
else:
|
|
210
|
+
closer()
|
|
211
|
+
except BaseException as error:
|
|
212
|
+
self._invocation.finish(error)
|
|
213
|
+
raise
|
|
214
|
+
finally:
|
|
215
|
+
self._invocation.finish()
|
|
216
|
+
# Release DataLoader iterators/workers and their last batch promptly.
|
|
217
|
+
self._wrapped = self._iterator = None
|
|
218
|
+
|
|
219
|
+
def __getattr__(self, name):
|
|
220
|
+
return getattr(self._wrapped, name)
|
|
221
|
+
|
|
222
|
+
def __del__(self):
|
|
223
|
+
self._invocation.finish()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
class _TracedDataset(ObjectProxy):
|
|
227
|
+
"""Keep upstream dataset protocols, with a separate span per traversal.
|
|
228
|
+
|
|
229
|
+
PipelineIterator is an IterableDataset whose __iter__ resets its producer,
|
|
230
|
+
including during a partial traversal. A one-shot Iterator wrapper loses that
|
|
231
|
+
behavior. ObjectProxy preserves __len__ and isinstance(..., IterableDataset),
|
|
232
|
+
which DataLoader uses to choose iterable rather than indexed access.
|
|
233
|
+
"""
|
|
234
|
+
def __init__(self, wrapped, invocation):
|
|
235
|
+
super().__init__(wrapped)
|
|
236
|
+
# This template stays unstarted and retains only metadata and context,
|
|
237
|
+
# not source previews, outputs, or a pipeline/model reference.
|
|
238
|
+
self._self_template = invocation
|
|
239
|
+
self._self_cursor = _TracedIterator(wrapped, copy(invocation))
|
|
240
|
+
|
|
241
|
+
def __iter__(self):
|
|
242
|
+
self._self_cursor.close()
|
|
243
|
+
invocation = copy(self._self_template)
|
|
244
|
+
try:
|
|
245
|
+
iterator = iter(self.__wrapped__)
|
|
246
|
+
except BaseException as error:
|
|
247
|
+
invocation.start()
|
|
248
|
+
invocation.finish(error)
|
|
249
|
+
raise
|
|
250
|
+
self._self_cursor = _TracedIterator(self.__wrapped__, invocation, iterator=iterator)
|
|
251
|
+
return self
|
|
252
|
+
|
|
253
|
+
def __next__(self):
|
|
254
|
+
return next(self._self_cursor)
|
|
255
|
+
|
|
256
|
+
def __reduce_ex__(self, protocol):
|
|
257
|
+
# Pickle the native dataset, not this proxy: its invocation retains a
|
|
258
|
+
# tracer provider and parent context which cannot safely cross a spawn
|
|
259
|
+
# boundary (and may contain unpicklable locks or private metadata).
|
|
260
|
+
# Inference in a deserialized DataLoader worker is deliberately untraced.
|
|
261
|
+
return self.__wrapped__.__reduce_ex__(protocol)
|
|
262
|
+
|
|
263
|
+
def close(self):
|
|
264
|
+
# Explicit close ends the current traversal; a later iter() retains
|
|
265
|
+
# the underlying dataset's own ability (or inability) to start again.
|
|
266
|
+
self._self_cursor.close()
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _input(args, kwargs):
|
|
270
|
+
if args:
|
|
271
|
+
return args[0]
|
|
272
|
+
for key in ("inputs", "text_inputs", "texts", "X"):
|
|
273
|
+
if key in kwargs:
|
|
274
|
+
return kwargs[key]
|
|
275
|
+
# Extractive QA uses named question/context rather than a single input.
|
|
276
|
+
return {key: kwargs[key] for key in ("question", "context") if key in kwargs}
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _lazy_input(value, task):
|
|
280
|
+
if task == "question-answering":
|
|
281
|
+
# Upstream's QA argument handler materializes its inputs eagerly.
|
|
282
|
+
return False
|
|
283
|
+
from torch.utils.data import Dataset
|
|
284
|
+
return isinstance(value, (GeneratorType, Dataset))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
class _PipelineWrapper:
|
|
288
|
+
def __init__(self, *, tracer: FITracer, config: TraceConfig, capture: bool, task: str, kind: str) -> None:
|
|
289
|
+
self.tracer, self.config, self.capture = tracer, config, capture
|
|
290
|
+
self.task, self.kind = task, kind
|
|
291
|
+
self.enabled = True
|
|
292
|
+
|
|
293
|
+
def __call__(self, wrapped: Callable[..., Any], instance: Any,
|
|
294
|
+
args: tuple[Any, ...], kwargs: dict[str, Any]) -> Any:
|
|
295
|
+
if not self.enabled or id(instance) in _ACTIVE.get() or context.get_value(_SUPPRESS_INSTRUMENTATION_KEY):
|
|
296
|
+
return wrapped(*args, **kwargs)
|
|
297
|
+
try:
|
|
298
|
+
value = _input(args, kwargs)
|
|
299
|
+
invocation = _Invocation(self, instance, value, context.get_current())
|
|
300
|
+
lazy = _lazy_input(value, self.task)
|
|
301
|
+
except Exception as error:
|
|
302
|
+
_log_failure("prepare span metadata", error)
|
|
303
|
+
return wrapped(*args, **kwargs)
|
|
304
|
+
if lazy:
|
|
305
|
+
# The framework sets up the iterator here but does not run inference.
|
|
306
|
+
# Guard super().__call__ wrappers without creating a premature span.
|
|
307
|
+
active = _ACTIVE.set((*_ACTIVE.get(), id(instance)))
|
|
308
|
+
call_start = time_ns()
|
|
309
|
+
try:
|
|
310
|
+
result = wrapped(*args, **kwargs)
|
|
311
|
+
except BaseException as error:
|
|
312
|
+
invocation.start(start_time=call_start)
|
|
313
|
+
invocation.finish(error)
|
|
314
|
+
raise
|
|
315
|
+
finally:
|
|
316
|
+
_ACTIVE.reset(active)
|
|
317
|
+
if isinstance(result, Iterator):
|
|
318
|
+
# Do not keep even bounded source previews for lazy streams.
|
|
319
|
+
invocation.attributes.pop("input.value", None)
|
|
320
|
+
from torch.utils.data import IterableDataset
|
|
321
|
+
if isinstance(result, IterableDataset):
|
|
322
|
+
return _TracedDataset(result, invocation)
|
|
323
|
+
return _TracedIterator(result, invocation)
|
|
324
|
+
# Some framework/backend paths materialize the result; avoid
|
|
325
|
+
# consuming or transforming it to manufacture lazy behavior.
|
|
326
|
+
invocation.start(start_time=call_start)
|
|
327
|
+
invocation.finish(output=result)
|
|
328
|
+
return result
|
|
329
|
+
try:
|
|
330
|
+
with invocation.activate():
|
|
331
|
+
result = wrapped(*args, **kwargs)
|
|
332
|
+
except BaseException as error:
|
|
333
|
+
invocation.finish(error)
|
|
334
|
+
raise
|
|
335
|
+
invocation.finish(output=result)
|
|
336
|
+
return result
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0" # x-release-please-version
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: traceAI-transformers
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenTelemetry instrumentation for local Hugging Face Transformers pipelines
|
|
5
|
+
Project-URL: Repository, https://github.com/future-agi/traceAI/tree/dev/python/frameworks/transformers
|
|
6
|
+
Project-URL: Documentation, https://docs.futureagi.com/docs/tracing
|
|
7
|
+
Author-email: Future AGI <no-reply@futureagi.com>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Requires-Python: <3.14,>=3.10
|
|
10
|
+
Requires-Dist: fi-instrumentation-otel<2,>=1.2.2
|
|
11
|
+
Requires-Dist: opentelemetry-instrumentation<1,>=0.50b0
|
|
12
|
+
Requires-Dist: transformers<6,>=4.42
|
|
13
|
+
Requires-Dist: wrapt<2,>=1.15
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: mypy<2,>=1.15; extra == 'dev'
|
|
16
|
+
Requires-Dist: pytest<9,>=8; extra == 'dev'
|
|
17
|
+
Requires-Dist: torch<3,>=2.6; extra == 'dev'
|
|
18
|
+
Requires-Dist: traceai-huggingface>=0.1.0; extra == 'dev'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# traceAI Transformers
|
|
22
|
+
|
|
23
|
+
Trace local Hugging Face Transformers pipeline inference in Future AGI. For remote Hugging Face InferenceClient calls, use `traceAI-huggingface` instead; the two instrumentors can run together.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install traceAI-transformers torch
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Python 3.10–3.13 and Transformers 4.42–5.x are supported. Transformers 5 requires Python 3.11 or later. Use PyTorch pipelines; select the appropriate PyTorch build for your CPU or GPU.
|
|
32
|
+
|
|
33
|
+
## Trace inference
|
|
34
|
+
|
|
35
|
+
Set `FI_API_KEY` and `FI_SECRET_KEY` in your environment, then configure tracing before calling your pipeline:
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
import os
|
|
39
|
+
from fi_instrumentation import register
|
|
40
|
+
from fi_instrumentation.fi_types import ProjectType
|
|
41
|
+
from traceai_transformers import TransformersInstrumentor
|
|
42
|
+
from transformers import pipeline
|
|
43
|
+
|
|
44
|
+
provider = register(
|
|
45
|
+
project_name="local-transformers",
|
|
46
|
+
project_type=ProjectType.OBSERVE,
|
|
47
|
+
headers={
|
|
48
|
+
"FI_API_KEY": os.environ["FI_API_KEY"],
|
|
49
|
+
"FI_SECRET_KEY": os.environ["FI_SECRET_KEY"],
|
|
50
|
+
},
|
|
51
|
+
verbose=False,
|
|
52
|
+
)
|
|
53
|
+
TransformersInstrumentor().instrument(tracer_provider=provider)
|
|
54
|
+
|
|
55
|
+
generate = pipeline(
|
|
56
|
+
"text-generation",
|
|
57
|
+
model="HuggingFaceTB/SmolLM2-135M-Instruct",
|
|
58
|
+
device=-1,
|
|
59
|
+
)
|
|
60
|
+
result = generate(
|
|
61
|
+
[{"role": "user", "content": "Explain what a tokenizer does in one sentence."}],
|
|
62
|
+
max_new_tokens=32,
|
|
63
|
+
do_sample=False,
|
|
64
|
+
)
|
|
65
|
+
print(result[0]["generated_text"][-1]["content"])
|
|
66
|
+
provider.force_flush()
|
|
67
|
+
provider.shutdown()
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The first run downloads the model. `register` exports OTLP traces to `https://api.futureagi.com/tracer/v1/traces` with `FI_API_KEY` and `FI_SECRET_KEY` headers. This is a traces-only endpoint, not a destination for OTLP metrics or logs. Open the `local-transformers` project in Future AGI to inspect `transformers.text-generation` spans.
|
|
71
|
+
|
|
72
|
+
## Content and privacy
|
|
73
|
+
|
|
74
|
+
By default, only task, model architecture, timing, status and error information are recorded. Model paths, generation parameters, embedding vectors and token usage estimates are not collected.
|
|
75
|
+
|
|
76
|
+
To include bounded input/output previews:
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
from fi_instrumentation import TraceConfig
|
|
80
|
+
|
|
81
|
+
TransformersInstrumentor().instrument(
|
|
82
|
+
tracer_provider=provider,
|
|
83
|
+
capture_content=True,
|
|
84
|
+
config=TraceConfig(hide_error_details=True),
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Choose this configuration on your first `instrument()` call, or call `uninstrument()` before changing it. Previews are limited to 4,096 characters and may be truncated. `TraceConfig` input/output privacy flags override content capture. Hiding inputs also suppresses output previews, because pipeline responses can contain the original prompt or conversation. Set `FI_HIDE_ERROR_DETAILS=true` to omit exception messages, stack traces and error descriptions. Error details can contain inputs even when content capture is off.
|
|
89
|
+
|
|
90
|
+
## Supported calls
|
|
91
|
+
|
|
92
|
+
| Pipeline | Span kind | Versions |
|
|
93
|
+
| --- | --- | --- |
|
|
94
|
+
| Text generation, including chat, batches and streamers | LLM | 4.42–5.x |
|
|
95
|
+
| Feature extraction, including tensor results | EMBEDDING | 4.42–5.x |
|
|
96
|
+
| Text-to-text generation, summarization and translation | LLM | 4.x only |
|
|
97
|
+
| Extractive question answering | CHAIN | 4.x only |
|
|
98
|
+
|
|
99
|
+
Transformers 5 removed the historical text-to-text, summarization, translation and extractive question-answering pipelines. Use a text-generation chat model on 5.x. Direct `model.generate()` calls, other tasks, TensorFlow pipelines, and model loading are not instrumented.
|
|
100
|
+
|
|
101
|
+
Generator and dataset calls remain lazy. Each traversal gets a span that starts when the first result is requested and ends on exhaustion, error or explicit `iterator.close()`. Dataset results preserve their length and `DataLoader` compatibility. Starting another traversal preserves the dataset's reset behavior and ends any unfinished span from the previous traversal. The span is current only while producing the next result, not while your application handles it. Close partially consumed iterators to end their spans promptly. Lazy payloads are not collected. Multiprocessing `DataLoader` workers receive the native dataset through serialization, not the instrumentor's tracer provider or parent context; inference in those workers is not traced by this dataset wrapper. To trace inference inside a worker, initialize its own exporter there and add explicit spans around that worker's inference; the parent instrumentor does not trace a deserialized dataset. Streamer spans end when pipeline inference returns, independently of when queued tokens are consumed. When starting a worker thread, propagate OpenTelemetry context explicitly if it should inherit a parent span.
|
|
102
|
+
|
|
103
|
+
Instrumentation is idempotent and supports `TransformersInstrumentor().uninstrument()` and re-instrumentation. It uses your supplied tracer provider without replacing the global provider.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
traceai_transformers/__init__.py,sha256=A1tsx1bgfDu6tp1p-AMyCeIA-QQx6UcdmdMGdcIAn28,3362
|
|
2
|
+
traceai_transformers/_wrappers.py,sha256=gbv0x_fiSmXWgDkEsEn8hoZpA-x--W5kQChHQX7Ow5E,13586
|
|
3
|
+
traceai_transformers/version.py,sha256=IOEzhCbX916JIkvZr03GFY4bl9YqSkfjdqLO4qJ2FOs,50
|
|
4
|
+
traceai_transformers-0.1.0.dist-info/METADATA,sha256=DDlSVwwhkX5S6tS_qQT3q2VSpVE45IAkFv21gW37bac,5658
|
|
5
|
+
traceai_transformers-0.1.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
6
|
+
traceai_transformers-0.1.0.dist-info/entry_points.txt,sha256=eMW-1VNv3gPxiIxD6HA4RuSx04B8D0_XUXrnhA6jVZQ,90
|
|
7
|
+
traceai_transformers-0.1.0.dist-info/RECORD,,
|