Skip to content

Commit 90eabee

Browse files
Fix UsageTracker AttributeError when using ParallelExecutor with dspy.context (#9095)
* Initial plan * Fix UsageTracker AttributeError when using ParallelExecutor with dspy.context Co-authored-by: chenmoneygithub <22925031+chenmoneygithub@users.noreply.github.com> * Address code review: check new_overrides for usage_tracker instead of parent_overrides Co-authored-by: chenmoneygithub <22925031+chenmoneygithub@users.noreply.github.com> * better tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: chenmoneygithub <22925031+chenmoneygithub@users.noreply.github.com> Co-authored-by: chenmoneygithub <chen.qian@databricks.com>
1 parent 95e3815 commit 90eabee

File tree

2 files changed

+61
-3
lines changed

2 files changed

+61
-3
lines changed

dspy/utils/parallelizer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,11 @@ def worker(parent_overrides, submission_id, index, item):
8989
from dspy.dsp.utils.settings import thread_local_overrides
9090

9191
original = thread_local_overrides.get()
92-
token = thread_local_overrides.set({**original, **parent_overrides.copy()})
93-
if parent_overrides.get("usage_tracker"):
92+
new_overrides = {**original, **parent_overrides.copy()}
93+
if new_overrides.get("usage_tracker"):
9494
# Usage tracker needs to be deep copied across threads so that each thread tracks its own usage
95-
thread_local_overrides.overrides["usage_tracker"] = copy.deepcopy(parent_overrides["usage_tracker"])
95+
new_overrides["usage_tracker"] = copy.deepcopy(new_overrides["usage_tracker"])
96+
token = thread_local_overrides.set(new_overrides)
9697

9798
try:
9899
return index, function(item)

tests/utils/test_usage_tracker.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from unittest import mock
2+
13
from pydantic import BaseModel
24

35
import dspy
@@ -325,3 +327,58 @@ class PromptTokensDetailsWrapper(BaseModel):
325327
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["audio_tokens"] == 1
326328
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["accepted_prediction_tokens"] == 1
327329
assert total_usage["gpt-4o-mini"]["completion_tokens_details"]["rejected_prediction_tokens"] == 1
330+
331+
332+
def test_parallel_executor_with_usage_tracker():
333+
"""Test that usage tracking works correctly with ParallelExecutor and mocked LM calls."""
334+
335+
parent_tracker = UsageTracker()
336+
337+
# Mock LM with different responses
338+
mock_lm = mock.MagicMock(spec=dspy.LM)
339+
mock_lm.return_value = ['{"answer": "Mocked answer"}']
340+
mock_lm.kwargs = {}
341+
mock_lm.model = "openai/gpt-4o-mini"
342+
343+
dspy.configure(lm=mock_lm, adapter=dspy.JSONAdapter())
344+
345+
def task1():
346+
# Simulate LM usage tracking for task 1
347+
dspy.settings.usage_tracker.add_usage(
348+
"openai/gpt-4o-mini",
349+
{
350+
"prompt_tokens": 50,
351+
"completion_tokens": 10,
352+
"total_tokens": 60,
353+
},
354+
)
355+
return dspy.settings.usage_tracker.get_total_tokens()
356+
357+
def task2():
358+
# Simulate LM usage tracking for task 2 with different values
359+
dspy.settings.usage_tracker.add_usage(
360+
"openai/gpt-4o-mini",
361+
{
362+
"prompt_tokens": 80,
363+
"completion_tokens": 15,
364+
"total_tokens": 95,
365+
},
366+
)
367+
return dspy.settings.usage_tracker.get_total_tokens()
368+
369+
# Execute tasks in parallel
370+
with dspy.context(track_usage=True, usage_tracker=parent_tracker):
371+
executor = dspy.Parallel()
372+
results = executor([(task1, {}), (task2, {})])
373+
# Verify that the two workers had different usage
374+
usage1 = results[0]
375+
usage2 = results[1]
376+
377+
# Task 1 should have 50 prompt tokens, task 2 should have 80
378+
assert usage1["openai/gpt-4o-mini"]["prompt_tokens"] == 50
379+
assert usage1["openai/gpt-4o-mini"]["completion_tokens"] == 10
380+
assert usage2["openai/gpt-4o-mini"]["prompt_tokens"] == 80
381+
assert usage2["openai/gpt-4o-mini"]["completion_tokens"] == 15
382+
383+
# Parent tracker should remain unchanged (workers have independent copies)
384+
assert len(parent_tracker.usage_data) == 0

0 commit comments

Comments
 (0)