Skip to content

Commit 782a3b3

Browse files
authored
Merge pull request #1 from wild-edge/feature/tensorflow-integration
Tensorflow integration
2 parents 299dd76 + 1bab629 commit 782a3b3

7 files changed

Lines changed: 566 additions & 128 deletions

File tree

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,13 @@ Call `client.instrument()` to activate auto-tracking for a supported library. Mo
3232

3333
See the `examples/` folder for complete working examples.
3434

35+
### Integration initialization
36+
37+
Initialize integrations at process startup, before model loading begins. Instrumentation patches are applied per process and should be installed before imports and constructor calls on instrumented libraries.
38+
39+
For high-priority paths, keep explicit registration with `client.load(...)` or `client.register_model(...)` as a fallback when model creation does not go through a patched API.
40+
For an explicit fallback pattern, see `examples/gguf_gemma_manual_example.py`.
41+
3542
### PyTorch (custom models)
3643

3744
PyTorch models are user-defined subclasses, so there is no single constructor to patch. Use `client.load()` to time construction and track load/unload automatically; inference is tracked via forward hooks once the model is registered.
@@ -67,6 +74,19 @@ outputs = session.run(None, {"input": image}) # tracked automatically
6774

6875
See `examples/onnx_example.py` for a complete example.
6976

77+
### TensorFlow
78+
79+
```python
80+
import tensorflow as tf
81+
82+
client.instrument("tensorflow")
83+
84+
model = tf.keras.models.load_model("model.keras") # tracked automatically
85+
output = model(batch, training=False) # tracked automatically
86+
```
87+
88+
See `examples/tensorflow_example.py` for a complete example.
89+
7090
### timm
7191

7292
```python

examples/tensorflow_example.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# /// script
2+
# requires-python = ">=3.10"
3+
# dependencies = ["wildedge-sdk", "tensorflow", "numpy"]
4+
#
5+
# [tool.uv.sources]
6+
# wildedge-sdk = { path = "..", editable = true }
7+
# ///
8+
"""TensorFlow integration example. Run with: uv run tensorflow_example.py."""
9+
10+
from __future__ import annotations
11+
12+
from pathlib import Path
13+
from tempfile import TemporaryDirectory
14+
15+
import numpy as np
16+
import tensorflow as tf
17+
18+
import wildedge
19+
20+
client = wildedge.WildEdge(
21+
app_version="1.0.0", # set WILDEDGE_DSN env var
22+
)
23+
client.instrument("tensorflow")
24+
25+
26+
def build_and_save_model(save_path: Path) -> None:
27+
model = tf.keras.Sequential(
28+
[
29+
tf.keras.layers.Input(shape=(16,)),
30+
tf.keras.layers.Dense(32, activation="relu"),
31+
tf.keras.layers.Dense(8),
32+
]
33+
)
34+
# Trigger variable creation before save.
35+
_ = model(np.zeros((1, 16), dtype=np.float32))
36+
model.save(save_path)
37+
38+
39+
with TemporaryDirectory() as temp_dir:
40+
model_path = Path(temp_dir) / "demo_model.keras"
41+
build_and_save_model(model_path)
42+
43+
# load_model is auto-instrumented by client.instrument("tensorflow")
44+
loaded = tf.keras.models.load_model(model_path)
45+
46+
batch = np.random.randn(4, 16).astype(np.float32)
47+
output = loaded(batch, training=False)
48+
print("output shape:", tuple(output.shape))
49+
50+
51+
client.close()

tests/test_integrations.py

Lines changed: 1 addition & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
"""Tests for ONNX, GGUF, PyTorch, and Keras integration extractors."""
1+
"""Tests for ONNX, GGUF, and Keras integration extractors."""
22

33
from __future__ import annotations
44

@@ -24,15 +24,6 @@
2424
from wildedge.integrations.onnx import (
2525
_detect_quantization as onnx_detect_quantization,
2626
)
27-
from wildedge.integrations.pytorch import (
28-
PytorchExtractor,
29-
)
30-
from wildedge.integrations.pytorch import (
31-
_detect_accelerator as torch_detect_accelerator,
32-
)
33-
from wildedge.integrations.pytorch import (
34-
_detect_quantization as torch_detect_quantization,
35-
)
3627
from wildedge.model import ModelHandle, ModelInfo
3728

3829
# ---------------------------------------------------------------------------
@@ -97,41 +88,6 @@ def __call__(self, *args, **kwargs):
9788
raise RuntimeError("cuda oom")
9889

9990

100-
# PyTorch
101-
102-
103-
class _TorchBase:
104-
"""Looks like torch.nn.Module to the MRO check."""
105-
106-
107-
_TorchBase.__name__ = "Module"
108-
_TorchBase.__module__ = "torch.nn.modules.module"
109-
110-
111-
class _FakeParam:
112-
class _Device:
113-
type = "cpu"
114-
115-
device = _Device()
116-
dtype = "torch.float32"
117-
118-
119-
class _FakeTorchModel(_TorchBase):
120-
def parameters(self):
121-
yield _FakeParam()
122-
123-
def modules(self):
124-
return iter([self])
125-
126-
def register_forward_pre_hook(self, hook):
127-
self._pre_hook = hook
128-
return MagicMock()
129-
130-
def register_forward_hook(self, hook):
131-
self._post_hook = hook
132-
return MagicMock()
133-
134-
13591
# Keras
13692

13793

@@ -309,78 +265,6 @@ def test_install_hooks_tracks_error_on_exception(self, publish_spy):
309265
assert publish_spy.events[0]["event_type"] == "error"
310266

311267

312-
# ---------------------------------------------------------------------------
313-
# PyTorch
314-
# ---------------------------------------------------------------------------
315-
316-
317-
class TestPytorchExtractor:
318-
extractor = PytorchExtractor()
319-
320-
def test_can_handle_torch_module(self):
321-
assert self.extractor.can_handle(_FakeTorchModel()) is True
322-
323-
def test_can_handle_rejects_plain_object(self):
324-
assert self.extractor.can_handle(object()) is False
325-
326-
def test_detect_accelerator_reads_parameter_device(self):
327-
model = _FakeTorchModel()
328-
assert torch_detect_accelerator(model) == "cpu"
329-
330-
def test_detect_accelerator_cuda(self):
331-
model = _FakeTorchModel()
332-
model.parameters = lambda: iter([MagicMock(device=MagicMock(type="cuda"))])
333-
assert torch_detect_accelerator(model) == "cuda"
334-
335-
def test_detect_accelerator_no_parameters_falls_back(self):
336-
model = _FakeTorchModel()
337-
model.parameters = lambda: iter([])
338-
assert isinstance(torch_detect_accelerator(model), str)
339-
340-
def test_detect_quantization_by_module_name(self):
341-
model = _FakeTorchModel()
342-
343-
class QuantizedLinear:
344-
pass
345-
346-
QuantizedLinear.__module__ = "torch.nn.quantized"
347-
model.modules = lambda: iter([QuantizedLinear()])
348-
model.parameters = lambda: iter([])
349-
assert torch_detect_quantization(model) == "int8"
350-
351-
def test_detect_quantization_by_param_dtype(self):
352-
model = _FakeTorchModel()
353-
model.modules = lambda: iter([])
354-
model.parameters = lambda: iter([MagicMock(dtype="torch.float16")])
355-
assert torch_detect_quantization(model) == "f16"
356-
357-
def test_extract_info_uses_class_name_as_model_id(self):
358-
model = _FakeTorchModel()
359-
model_id, info = self.extractor.extract_info(model, {})
360-
assert model_id == "_FakeTorchModel"
361-
assert info.model_format == "pytorch"
362-
363-
def test_extract_info_override_model_id(self):
364-
model = _FakeTorchModel()
365-
model_id, _ = self.extractor.extract_info(model, {"id": "my-resnet"})
366-
assert model_id == "my-resnet"
367-
368-
def test_install_hooks_publishes_inference(self, publish_spy):
369-
model = _FakeTorchModel()
370-
handle = make_handle(publish_spy)
371-
self.extractor.install_hooks(model, handle)
372-
model._pre_hook(model, (None,))
373-
model._post_hook(model, (None,), None)
374-
assert len(publish_spy.events) == 1
375-
assert publish_spy.events[0]["event_type"] == "inference"
376-
377-
def test_install_hooks_sets_detected_accelerator(self, publish_spy):
378-
model = _FakeTorchModel()
379-
handle = make_handle(publish_spy)
380-
self.extractor.install_hooks(model, handle)
381-
assert handle.detected_accelerator == "cpu"
382-
383-
384268
# ---------------------------------------------------------------------------
385269
# Keras
386270
# ---------------------------------------------------------------------------

tests/test_integrations_pytorch.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
"""Tests for PyTorch integration extractor."""
2+
3+
from __future__ import annotations
4+
5+
from unittest.mock import MagicMock
6+
7+
from wildedge.integrations.pytorch import (
8+
PytorchExtractor,
9+
)
10+
from wildedge.integrations.pytorch import (
11+
_detect_accelerator as torch_detect_accelerator,
12+
)
13+
from wildedge.integrations.pytorch import (
14+
_detect_quantization as torch_detect_quantization,
15+
)
16+
from wildedge.model import ModelHandle, ModelInfo
17+
18+
19+
def make_handle(publish_spy) -> ModelHandle:
20+
info = ModelInfo(
21+
model_name="test",
22+
model_version="1.0",
23+
model_source="local",
24+
model_format="test",
25+
)
26+
return ModelHandle(model_id="m", info=info, publish=publish_spy)
27+
28+
29+
class _TorchBase:
30+
"""Looks like torch.nn.Module to the MRO check."""
31+
32+
33+
_TorchBase.__name__ = "Module"
34+
_TorchBase.__module__ = "torch.nn.modules.module"
35+
36+
37+
class _FakeParam:
38+
class _Device:
39+
type = "cpu"
40+
41+
device = _Device()
42+
dtype = "torch.float32"
43+
44+
45+
class _FakeTorchModel(_TorchBase):
46+
def parameters(self):
47+
yield _FakeParam()
48+
49+
def modules(self):
50+
return iter([self])
51+
52+
def register_forward_pre_hook(self, hook):
53+
self._pre_hook = hook
54+
return MagicMock()
55+
56+
def register_forward_hook(self, hook):
57+
self._post_hook = hook
58+
return MagicMock()
59+
60+
61+
class TestPytorchExtractor:
62+
extractor = PytorchExtractor()
63+
64+
def test_can_handle_torch_module(self):
65+
assert self.extractor.can_handle(_FakeTorchModel()) is True
66+
67+
def test_can_handle_rejects_plain_object(self):
68+
assert self.extractor.can_handle(object()) is False
69+
70+
def test_detect_accelerator_reads_parameter_device(self):
71+
model = _FakeTorchModel()
72+
assert torch_detect_accelerator(model) == "cpu"
73+
74+
def test_detect_accelerator_cuda(self):
75+
model = _FakeTorchModel()
76+
model.parameters = lambda: iter([MagicMock(device=MagicMock(type="cuda"))])
77+
assert torch_detect_accelerator(model) == "cuda"
78+
79+
def test_detect_accelerator_no_parameters_falls_back(self):
80+
model = _FakeTorchModel()
81+
model.parameters = lambda: iter([])
82+
assert isinstance(torch_detect_accelerator(model), str)
83+
84+
def test_detect_quantization_by_module_name(self):
85+
model = _FakeTorchModel()
86+
87+
class QuantizedLinear:
88+
pass
89+
90+
QuantizedLinear.__module__ = "torch.nn.quantized"
91+
model.modules = lambda: iter([QuantizedLinear()])
92+
model.parameters = lambda: iter([])
93+
assert torch_detect_quantization(model) == "int8"
94+
95+
def test_detect_quantization_by_param_dtype(self):
96+
model = _FakeTorchModel()
97+
model.modules = lambda: iter([])
98+
model.parameters = lambda: iter([MagicMock(dtype="torch.float16")])
99+
assert torch_detect_quantization(model) == "f16"
100+
101+
def test_detect_quantization_by_param_dtype_bf16(self):
102+
model = _FakeTorchModel()
103+
model.modules = lambda: iter([])
104+
model.parameters = lambda: iter([MagicMock(dtype="torch.bfloat16")])
105+
assert torch_detect_quantization(model) == "bf16"
106+
107+
def test_detect_quantization_by_param_dtype_qint(self):
108+
model = _FakeTorchModel()
109+
model.modules = lambda: iter([])
110+
model.parameters = lambda: iter([MagicMock(dtype="torch.qint8")])
111+
assert torch_detect_quantization(model) == "int8"
112+
113+
def test_detect_quantization_by_param_dtype_quint(self):
114+
model = _FakeTorchModel()
115+
model.modules = lambda: iter([])
116+
model.parameters = lambda: iter([MagicMock(dtype="torch.quint8")])
117+
assert torch_detect_quantization(model) == "int8"
118+
119+
def test_detect_quantization_by_param_dtype_int8(self):
120+
model = _FakeTorchModel()
121+
model.modules = lambda: iter([])
122+
model.parameters = lambda: iter([MagicMock(dtype="torch.int8")])
123+
assert torch_detect_quantization(model) == "int8"
124+
125+
def test_detect_quantization_returns_none_when_unknown(self):
126+
model = _FakeTorchModel()
127+
model.modules = lambda: iter([])
128+
model.parameters = lambda: iter([MagicMock(dtype="torch.float32")])
129+
assert torch_detect_quantization(model) is None
130+
131+
def test_detect_quantization_returns_none_on_exception(self):
132+
model = _FakeTorchModel()
133+
model.modules = lambda: iter([])
134+
135+
def broken_parameters():
136+
raise RuntimeError("broken params")
137+
138+
model.parameters = broken_parameters
139+
assert torch_detect_quantization(model) is None
140+
141+
def test_extract_info_uses_class_name_as_model_id(self):
142+
model = _FakeTorchModel()
143+
model_id, info = self.extractor.extract_info(model, {})
144+
assert model_id == "_FakeTorchModel"
145+
assert info.model_format == "pytorch"
146+
147+
def test_extract_info_override_model_id(self):
148+
model = _FakeTorchModel()
149+
model_id, _ = self.extractor.extract_info(model, {"id": "my-resnet"})
150+
assert model_id == "my-resnet"
151+
152+
def test_install_hooks_publishes_inference(self, publish_spy):
153+
model = _FakeTorchModel()
154+
handle = make_handle(publish_spy)
155+
self.extractor.install_hooks(model, handle)
156+
model._pre_hook(model, (None,))
157+
model._post_hook(model, (None,), None)
158+
assert len(publish_spy.events) == 1
159+
assert publish_spy.events[0]["event_type"] == "inference"
160+
161+
def test_install_hooks_sets_detected_accelerator(self, publish_spy):
162+
model = _FakeTorchModel()
163+
handle = make_handle(publish_spy)
164+
self.extractor.install_hooks(model, handle)
165+
assert handle.detected_accelerator == "cpu"

0 commit comments

Comments
 (0)