Skip to content

Commit 604f0d9

Browse files
authored
Support for inference hardware context (#16)
1 parent f552179 commit 604f0d9

45 files changed

Lines changed: 1628 additions & 385 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def run(input):
9999
| `enable_offline_persistence` | `true` | - | Persist unsent events to disk and replay on restart |
100100
| `max_event_age_sec` | `900` | - | Max age before dead-lettering |
101101
| `enable_dead_letter_persistence` | `false` | - | Persist dropped batches to disk |
102+
| `sampling_interval_s` | `30.0` | `WILDEDGE_SAMPLING_INTERVAL_S` | Seconds between background hardware snapshots; `0` or `None` to disable |
102103

103104
## Privacy
104105

examples/chatgpt_example.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
# [tool.uv.sources]
66
# wildedge-sdk = { path = "..", editable = true }
77
# ///
8-
"""ChatGPT (OpenAI API): fully manual integration.
8+
"""ChatGPT (OpenAI API): fully manual integration with explicit hardware capture.
99
1010
Shows how to instrument a remote LLM with no local model file.
1111
Tracks input/output token counts, generation config, latency, errors,
12-
and user feedback without any auto-instrumentation hooks.
12+
and user feedback without any auto-instrumentation hooks. The background
13+
hardware sampler is disabled; hardware context is captured explicitly via
14+
capture_hardware() and passed to track_inference().
1315
1416
Run with: uv run chatgpt_example.py
1517
Requires: WILDEDGE_DSN and OPENAI_API_KEY environment variables.
@@ -18,14 +20,21 @@
1820
from openai import OpenAI
1921

2022
import wildedge
21-
from wildedge import FeedbackType, GenerationConfig, GenerationOutputMeta, TextInputMeta
23+
from wildedge import (
24+
FeedbackType,
25+
GenerationConfig,
26+
GenerationOutputMeta,
27+
TextInputMeta,
28+
capture_hardware,
29+
)
2230
from wildedge.timing import Timer
2331

2432
MODEL = "gpt-4o"
2533
MODEL_VERSION = "2024-08-06"
2634

2735
client = wildedge.WildEdge(
2836
app_version="1.0.0", # set WILDEDGE_DSN env var
37+
sampling_interval_s=None, # disabled: hardware captured explicitly per call
2938
)
3039

3140
# Remote models have no local object to inspect, so register with a
@@ -72,6 +81,7 @@
7281

7382
inference_id = handle.track_inference(
7483
duration_ms=t.elapsed_ms,
84+
hardware=capture_hardware(),
7585
input_modality="text",
7686
output_modality="text",
7787
success=True,
Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55
# [tool.uv.sources]
66
# wildedge-sdk = { path = "..", editable = true }
77
# ///
8-
"""Gemma 2 GGUF: fully manual integration, no auto-instrumentation.
8+
"""Gemma 2 GGUF: fully manual integration with background hardware sampling.
99
1010
Shows explicit download / load / inference / error tracking without
11-
client.instrument() or any automatic hooks.
11+
client.instrument() or any automatic hooks. Hardware context is captured
12+
automatically on every track_inference() call via the background sampler
13+
started by WildEdge (sampling_interval_s=30 by default).
1214
13-
Run with: uv run gguf_gemma_manual_example.py
15+
Run with: uv run gguf_gemma_example.py
1416
"""
1517

1618
import os
@@ -19,7 +21,7 @@
1921
from llama_cpp import Llama
2022

2123
import wildedge
22-
from wildedge.events.inference import GenerationOutputMeta, TextInputMeta
24+
from wildedge import GenerationOutputMeta, TextInputMeta
2325
from wildedge.timing import Timer
2426

2527
REPO = "bartowski/gemma-2-2b-it-GGUF"

examples/transformers_example.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,23 @@
2121
from __future__ import annotations
2222

2323
import argparse
24+
import platform
2425

2526
from transformers import pipeline
2627

2728
import wildedge
2829

30+
_DEVICE = "mps" if platform.machine() == "arm64" else "cpu"
31+
2932

3033
def run_classify() -> None:
3134
pipe = pipeline(
3235
"text-classification",
3336
model="distilbert-base-uncased-finetuned-sst-2-english",
37+
device=_DEVICE,
3438
)
3539
inputs = [
36-
"I absolutely loved this film the performances were outstanding!",
40+
"I absolutely loved this film, the performances were outstanding!",
3741
"The service was awful and the food arrived cold.",
3842
"An average experience, nothing special either way.",
3943
]
@@ -47,7 +51,7 @@ def run_classify() -> None:
4751

4852

4953
def run_generate() -> None:
50-
pipe = pipeline("text-generation", model="gpt2", max_new_tokens=40)
54+
pipe = pipeline("text-generation", model="gpt2", max_new_tokens=40, device=_DEVICE)
5155
prompts = [
5256
"The future of on-device AI is",
5357
"Once upon a time, a small robot learned",
@@ -60,7 +64,7 @@ def run_generate() -> None:
6064

6165

6266
def run_embed() -> None:
63-
pipe = pipeline("feature-extraction", model="bert-base-uncased")
67+
pipe = pipeline("feature-extraction", model="bert-base-uncased", device=_DEVICE)
6468
sentences = [
6569
"Machine learning is transforming every industry.",
6670
"On-device inference keeps your data private.",
@@ -88,8 +92,6 @@ def main() -> None:
8892
)
8993
args = parser.parse_args()
9094

91-
# instrument() patches transformers.pipeline and AutoModel.from_pretrained
92-
# before any model is loaded; everything below is tracked automatically.
9395
client = wildedge.WildEdge(app_version="1.0.0") # set WILDEDGE_DSN env var
9496
client.instrument("transformers", hubs=["huggingface"])
9597

tests/compat/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import pytest
66

77
import wildedge
8-
from wildedge.device import DeviceInfo
8+
from wildedge.platforms.device_info import DeviceInfo
99

1010

1111
class _DummyConsumer:

tests/conftest.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,39 @@
11
"""Shared fixtures for the WildEdge SDK test suite."""
22

3+
import sys
34
from types import SimpleNamespace
45
from unittest.mock import MagicMock, patch
56

67
import pytest
78

89
from wildedge.client import WildEdge
9-
from wildedge.device import DeviceInfo
1010
from wildedge.model import ModelInfo
11+
from wildedge.platforms.device_info import DeviceInfo
12+
13+
14+
@pytest.fixture(autouse=True)
15+
def reset_hardware_sampler():
16+
yield
17+
from wildedge.platforms import stop_sampler
18+
19+
stop_sampler()
20+
21+
22+
PLATFORM_MARKS = {
23+
"requires_linux": "linux",
24+
"requires_macos": "darwin",
25+
"requires_windows": "win32",
26+
}
27+
28+
29+
def pytest_collection_modifyitems(items):
30+
for item in items:
31+
for mark_name, required_platform in PLATFORM_MARKS.items():
32+
if item.get_closest_marker(mark_name) and sys.platform != required_platform:
33+
item.add_marker(
34+
pytest.mark.skip(reason=f"requires {required_platform}")
35+
)
36+
break
1137

1238

1339
@pytest.fixture
@@ -71,5 +97,8 @@ def client_with_stubbed_runtime():
7197
patch("wildedge.client.Transmitter"),
7298
patch("wildedge.client.Consumer"),
7399
):
74-
client = WildEdge(dsn="https://secret@ingest.wildedge.dev/key")
100+
client = WildEdge(
101+
dsn="https://secret@ingest.wildedge.dev/key",
102+
sampling_interval_s=None,
103+
)
75104
return client

tests/test_batch.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from wildedge import constants
66
from wildedge.batch import build_batch
7-
from wildedge.device import DeviceInfo
7+
from wildedge.platforms.device_info import DeviceInfo
88

99

1010
def make_device() -> DeviceInfo:

tests/test_cli.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import json
44
import os
5+
from pathlib import Path
56

67
import pytest
78

@@ -152,7 +153,7 @@ def test_install_runtime_default_flush_timeout_is_shutdown_budget(monkeypatch):
152153
class FakeWildEdge:
153154
SUPPORTED_INTEGRATIONS = {"onnx"}
154155

155-
def __init__(self, *, dsn, app_version, debug): # type: ignore[no-untyped-def]
156+
def __init__(self, *, dsn, app_version, debug, sampling_interval_s=None): # type: ignore[no-untyped-def]
156157
pass
157158

158159
def instrument(self, name): # type: ignore[no-untyped-def]
@@ -182,7 +183,7 @@ def test_install_runtime_instruments_requested_integrations(monkeypatch):
182183
class FakeWildEdge:
183184
SUPPORTED_INTEGRATIONS = {"onnx", "torch"}
184185

185-
def __init__(self, *, dsn, app_version, debug): # type: ignore[no-untyped-def]
186+
def __init__(self, *, dsn, app_version, debug, sampling_interval_s=None): # type: ignore[no-untyped-def]
186187
assert dsn == "https://secret@ingest.wildedge.dev/key"
187188
assert app_version == "2.0.0"
188189
assert debug is True
@@ -218,7 +219,7 @@ def test_install_runtime_strict_integrations_raises(monkeypatch):
218219
class FakeWildEdge:
219220
SUPPORTED_INTEGRATIONS = {"onnx"}
220221

221-
def __init__(self, *, dsn, app_version, debug): # type: ignore[no-untyped-def]
222+
def __init__(self, *, dsn, app_version, debug, sampling_interval_s=None): # type: ignore[no-untyped-def]
222223
pass
223224

224225
def instrument(self, name): # type: ignore[no-untyped-def]
@@ -330,8 +331,8 @@ def test_doctor_uses_project_key_for_default_namespace(monkeypatch, capsys):
330331
rc = cli.main(["doctor", "--integrations", "onnx"])
331332
out = capsys.readouterr().out
332333
assert rc == 0
333-
assert "/test-prod/pending_queue" in out
334-
assert "/test-prod/dead_letters" in out
334+
assert str(Path("test-prod") / "pending_queue") in out
335+
assert str(Path("test-prod") / "dead_letters") in out
335336

336337

337338
def test_doctor_uses_app_identity_override_for_namespace(monkeypatch, capsys):
@@ -342,8 +343,8 @@ def test_doctor_uses_app_identity_override_for_namespace(monkeypatch, capsys):
342343
rc = cli.main(["doctor", "--integrations", "onnx"])
343344
out = capsys.readouterr().out
344345
assert rc == 0
345-
assert "/my-app/pending_queue" in out
346-
assert "/my-app/dead_letters" in out
346+
assert str(Path("my-app") / "pending_queue") in out
347+
assert str(Path("my-app") / "dead_letters") in out
347348

348349

349350
def test_runner_clears_runtime_env_when_no_propagate(monkeypatch):
@@ -368,7 +369,7 @@ def shutdown(self): # type: ignore[no-untyped-def]
368369

369370
def test_install_runtime_tracks_missing_dependency_status(monkeypatch):
370371
class FakeWildEdge:
371-
def __init__(self, *, dsn, app_version, debug): # type: ignore[no-untyped-def]
372+
def __init__(self, *, dsn, app_version, debug, sampling_interval_s=None): # type: ignore[no-untyped-def]
372373
pass
373374

374375
def instrument(self, name): # type: ignore[no-untyped-def]

tests/test_consumer.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33
import os
44
from unittest.mock import MagicMock, patch
55

6+
import pytest
7+
68
from wildedge import constants
79
from wildedge.consumer import Consumer
8-
from wildedge.device import DeviceInfo
10+
from wildedge.platforms.device_info import DeviceInfo
911
from wildedge.queue import EventQueue
1012
from wildedge.transmitter import IngestResponse, TransmitError, Transmitter
1113

@@ -435,6 +437,10 @@ def test_resume_registers_atexit(self, monkeypatch):
435437

436438

437439
class TestForkRegistration:
440+
@pytest.mark.skipif(
441+
not hasattr(os, "register_at_fork"),
442+
reason="os.register_at_fork not available on Windows",
443+
)
438444
def test_register_at_fork_wires_pause_and_resume(self, monkeypatch):
439445
"""WildEdge.__init__ registers _pause and _resume via os.register_at_fork."""
440446
from wildedge.client import WildEdge

0 commit comments

Comments
 (0)