설정과 context()

설정과 context() (Settings and context())

dspy.settings 은 언어 모델, 어댑터, retriever, 콜백, 그리고 다른 모든 구성 요소가 읽는 몇 가지 런타임 플래그를 담는 싱글턴(singleton) 입니다. 시작할 때 dspy.configure(...) 로 한 번 구성하고, with dspy.context(...) 로 로컬에서 오버라이드합니다. 이 페이지는 그 안에 무엇이 있는지, 전역·범위 계층이 어떻게 상호작용하는지, 오버라이드가 스레드와 async 작업에 어떻게(그리고 어떻게 안) 전파되는지를 다룹니다.

출처: 문서

본문

프로그램 중간에 LM을 바꾸거나, 서로 다른 분기를 다른 어댑터로 실행하거나, 병렬 워커가 컨텍스트 오버라이드를 못 보는 이유를 디버깅하거나, settings에서 읽어야 하는 새 모듈을 쓸 때 이 문서를 읽으세요.

설계 결정

1. 설계상 두 진입점

configure 는 프로세스 전역·일회성 설정용이고, context 는 범위 오버라이드용입니다. 동작과 소유 규칙이 다릅니다. configure 는 전역 dict를 변경하고, context 는 ContextVar에 푸시합니다. 이 분리는 각 호출의 의도를 분명히 하고, 전역에 대해 일회성 설정을 강제하면서 범위 오버라이드를 자유롭게 중첩할 수 있게 합니다.

2. 아래에는 두 계층

전역 main_thread_config dict와 thread_local_overrides ContextVar입니다. 읽기는 오버라이드를 먼저 확인한 뒤 전역을 봅니다. dspy.context(lm=alt_lm) 블록은 전역 설정을 건드리지 않고, 현재 실행 컨텍스트 동안 그것을 가립니다(shadow) . 모든 읽기가 같은 병합을 거치기 때문에, 컨텍스트 블록 안의 어댑터 교체는 블록이 열리기 전에 구성된 하위 모듈에도 존중됩니다.

3. configure 는 소유 스레드가 있다

처음 호출한 스레드가 소유하고, 다른 스레드의 호출은 RuntimeError 를 던집니다. 이는 configure 를 Python 모듈 수준 초기화처럼 보이게 하려는 의도입니다. 여러 스레드가 프로그램 중간에 재구성하면, 한 스레드의 lm 교체가 다른 스레드의 예측에 조용히 나타나는 경합이 생길 수 있어요. IPython은 예외입니다 — 인터랙티브 세션은 유연성이 필요하니까요.

4. context 는 Python의 contextvars 를 쓴다

오버라이드는 await 와 asyncio.create_task 를 가로질러 올바르게 전파됩니다. threading.local() 은 첫 await 에서 오버라이드를 잃었을 겁니다. contextvars 는 오버라이드를 스레드가 아니라 실행 컨텍스트에 묶고, 표준 라이브러리가 그 안에서 생성된 작업에 자동으로 복사합니다. 그래서 with dspy.context(lm=alt): await my_async_module(...) 이 예상대로 동작하는 것입니다.

5. DSPy의 실행자들은 스냅샷하고 다시 적용한다

dspy.Parallel, Module.batch, asyncify 는 부모의 오버라이드를 캡처해 각 워커 스레드 안에서 다시 설정합니다. 평범한 ContextVar 상속은 새 OS 스레드로 건너가면 살아남지 못합니다. Python은 asyncio 작업 경계에서만 자동 전파하니까요. 그래서 실행자들이 직접 합니다. 제출 시 thread_local_overrides.get() 을 잡고, 사용자 코드를 실행하기 전에 워커 안에서 .set() 하며, 나갈 때 재설정합니다. usage_tracker 는 워커마다 deep-copy되어 각 스레드가 공유 객체에서 경쟁하는 대신 독립적으로 회계합니다.

6. 평범한 Python 스레드는 오버라이드를 상속하지 않는다

threading.Thread 와 맨 concurrent.futures.ThreadPoolExecutor 는 당신의 with dspy.context(...) 블록을 보지 못합니다. 사람들이 자주 걸리는 부분입니다. 자신의 스레드 풀을 만들고 워커에서 DSPy 모듈을 실행하면 컨텍스트 오버라이드는 부모에 남아요. 해결책: 부모에서 dspy.settings.thread_local_overrides.get() 을 캡처해 각 워커 안에서 재설정하거나, 그 일을 대신 해 주는 dspy.Parallel 을 쓰세요.

7. 설정은 호출 지점에서 지연(lazily) 읽힌다

Predict는 호출 시점에 settings.lm 을 읽고, Adapter는 settings.adapter 를, Module은 settings.track_usage 를 읽습니다. constructor는 settings 인자를 받지 않습니다. 대가는 암묵적 의존성 — 모듈의 동작이 호출을 둘러싼 컨텍스트에 달려 있다는 것 — 이고, 장점은 조합입니다. 설정 오버라이드가 재배선 없이 모든 하위 모듈로 흘러갑니다.

8. 일부 손잡이는 settings가 아니라 dspy.LM 에 있다

temperature, max_tokens, api_base, 재시도. 그 분리는 의도적입니다. settings는 LM들에 걸쳐 적용되는 조율 손잡이용입니다 — 어떤 LM이 기본인지, 어떤 어댑터를 쓸지, 사용량을 추적하는지. LM 인스턴스 손잡이(샘플링 파라미터, 엔드포인트, 재시도 정책)는 LM 자체에 있는데, 프로그램이 서로 다른 설정의 여러 LM을 쓸 수 있기 때문입니다.

9. 잠금은 최소다

단일 Lock 이 _ensure_configure_allowed 만 보호합니다. 읽기는 동기화되지 않습니다. configure 동안 소유권 검사를 직렬화하고, 읽기 경로는 lock을 잡지 않습니다. CPython의 dict-항목 접근은 읽기 패턴에 충분히 원자적이고, 잠금을 더하면 모든 Predict 호출이 느려집니다. configure 는 시작할 때 한 번 호출되도록 되어 있고, 안정되면 읽기는 경쟁이 없습니다.

API 살펴보기

설정하기

dspy.configure(**kwargs) kwargs 를 전역 main_thread_config dict에 병합합니다. 소유 스레드 강제로 스레드 안전합니다. 다른 스레드의 호출은 RuntimeError 입니다. async 작업에도 같은 제한이 적용되며 IPython은 예외입니다. 시작 시 한 번 쓰고, 중간 교체는 context 를 선호하세요.

dspy.context(**kwargs) 컨텍스트 관리자. 진입 시 kwargs 를 thread_local_overrides ContextVar에 푸시하고, 종료 시 ContextVar 토큰을 재설정합니다. 어떤 스레드·async 작업에서도 안전하고 깔끔하게 중첩됩니다. 병렬 분기 안, asyncify-감싼 함수 안, 범위 오버라이드가 필요한 어디든 쓰세요.

dspy.settings 싱글턴. 속성을 직접 읽으세요: dspy.settings.lm, dspy.settings.adapter. 읽기는 매 접근마다 global + override를 병합합니다 — 캐싱이 없으므로 컨텍스트 블록의 변경이 읽는 모든 하위 모듈에 즉시 보입니다.

dspy.settings.copy() / dspy.settings.config 현재 유효 설정의 병합된 dotdict를 반환합니다. 설정을 스냅샷하려 할 때(워커에 넘기거나, 기록하거나, 전후 비교) 유용합니다.

dspy.load_settings(path, allow_pickle=False) / dspy.settings.save(path, modules_to_serialize=None, exclude_keys=None) 설정 dict를 cloudpickle로 디스크와 왕복시킵니다. exclude_keys 는 민감 필드(API 키는 settings에 없지만, 추가한 커스텀 키는 있을 수 있음)를 빼게 합니다.

무엇을 구성할 수 있는지

표준 키(DEFAULT_CONFIG 에 정의):

키 기본값 용도
lm None 모든 Predict 의 기본 LM.
adapter None (ChatAdapter() 로 폴백) 포맷·파싱에 쓰는 어댑터.
rm None dspy.Retrieve() 의 기본 retriever.
callbacks [] 모듈 호출 주변에서 발화하는 콜백 핸들러.
track_usage False 토큰 사용량 회계 활성화.
num_threads 8 dspy.Parallel / Module.batch 의 기본 스레드 수.
async_max_workers 8 asyncify 의 동시성 상한.
max_errors 10 취소 전 병렬 작업 오류 임계값.
provide_traceback False 병렬 오류 로그에 traceback 포함.
disable_history False 모듈의 호출 히스토리 기록 건너뛰기.
warn_on_type_mismatch True 입력이 시그니처 선언 타입과 안 맞을 때 경고.
max_history_size / max_trace_size 각 10000 모듈 히스토리와 호출별 트레이스의 트림 한도.
stream_listeners / send_stream [] / None 스트리밍 배선.
allow_tool_async_sync_conversion False sync 코드에서 async 도구 실행 허용.
branch_idx 0 일부 최적화기가 쓰는 롤아웃/브랜치 인덱스.
interpreter_factory None (PythonInterpreter 사용) 코드 실행 모듈의 기본 PythonInterpreter 팩토리 교체. 모듈 자신의 팩토리가 이기는데, 단 PythonInterpreter 일 때는 제외.

DSPy가 설정하는 키(직접 설정하지 마세요):

  • caller_predict, caller_modules — 현재 호출 스택(콜백·트레이싱용).
  • usage_tracker, trace — 관련 컨텍스트가 채우고 모듈 안에서 읽음.

오버라이드가 전파되는 방식

ContextVar 전파 — dspy.context(...) 는 contextvars.ContextVar 를 설정합니다. Python은 활성 컨텍스트를 asyncio.create_task, asyncio.gather 에 자동 복사하고 await 를 가로질러 상속합니다.

dspy.Parallel / Module.batch — 제출 시 부모의 thread_local_overrides.get() 을 캡처해 각 워커에 dict를 넘깁니다. 워커는 사용자 코드 실행 전 thread_local_overrides.set(parent_overrides) 를 호출하고 나갈 때 재설정합니다. usage_tracker 는 워커마다 deep-copy됩니다.

dspy.asyncify(fn) — anyio.to_thread.run_sync 로 sync DSPy 모듈을 워커 스레드에서 실행합니다. 같은 스냅샷-및-재적용 트릭: 호출 시점에 부모 오버라이드 캡처, 워커에서 재적용, 종료 시 재설정.

평범한 threading.Thread / concurrent.futures.ThreadPoolExecutor — 상속하지 않습니다. 해결책: 부모에서 캡처해 워커 안에서 set 하거나 dspy.Parallel 로 감싸세요.

소유권과 잠금

소유 스레드 — 첫 dspy.configure 호출이 config_owner_thread_id 를 기록합니다. 다른 스레드(또는 async의 다른 작업)의 이후 호출은 RuntimeError 입니다. 소유자의 여러 configure 호출은 허용되고 제자리 덮어씁니다.

IPython 예외 — 같은 IPython 세션의 다른 async 작업에서의 여러 configure 호출은 get_ipython() 으로 감지되어 허용됩니다.

잠금 — 단일 threading.Lock 이 _ensure_configure_allowed 를 보호합니다. 읽기 경로는 잡지 않습니다.

크로스링크

  • 저장과 불러오기 — 설정 직렬화가 모듈 직렬화 옆에 있음.
  • 참조: clients.md — settings에 의도적으로 없는 LM 인스턴스 손잡이(temperature, max_tokens, api_base, 재시도).

더 알아보기 (Learn more)