openaivec 1.0.10__tar.gz → 1.0.12__tar.gz
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.
- openaivec-1.0.12/.github/workflows/publish.yml +59 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/.github/workflows/test-pr.yml +14 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/PKG-INFO +8 -8
- {openaivec-1.0.10 → openaivec-1.0.12}/README.md +7 -7
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_cache/optimize.py +20 -5
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/_cache/test_optimize.py +78 -6
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/_cache/test_proxy_suggester.py +2 -1
- {openaivec-1.0.10 → openaivec-1.0.12}/uv.lock +237 -237
- openaivec-1.0.10/.github/workflows/publish.yml +0 -34
- {openaivec-1.0.10 → openaivec-1.0.12}/.env.example +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/.github/copilot-instructions.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/.github/dependabot.yml +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/.github/workflows/docs.yml +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/.github/workflows/test.yml +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/.gitignore +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/AGENTS.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/CODE_OF_CONDUCT.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/LICENSE +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/SECURITY.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/SUPPORT.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/main.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/pandas_ext.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/spark.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/task.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/customer_sentiment.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/inquiry_classification.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/inquiry_summary.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/intent_analysis.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/response_suggestion.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/customer_support/urgency_analysis.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/dependency_parsing.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/keyword_extraction.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/morphological_analysis.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/named_entity_recognition.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/sentiment_analysis.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/api/tasks/nlp/translation.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/contributor-guide.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/index.md +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/overrides/main.html +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/docs/robots.txt +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/mkdocs.yml +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/pyproject.toml +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/pytest.ini +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_cache/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_cache/proxy.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_di.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_embeddings.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_log.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_model.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_prompt.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_provider.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_responses.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_schema/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_schema/infer.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_schema/spec.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_serialize.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/_util.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/pandas_ext.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/spark.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/customer_sentiment.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/inquiry_classification.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/inquiry_summary.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/intent_analysis.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/response_suggestion.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/customer_support/urgency_analysis.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/dependency_parsing.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/keyword_extraction.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/morphological_analysis.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/named_entity_recognition.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/sentiment_analysis.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/nlp/translation.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/table/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/src/openaivec/task/table/fillna.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/__init__.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/_cache/test_proxy.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/_schema/test_infer.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/_schema/test_spec.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/conftest.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_di.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_embeddings.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_pandas_ext.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_prompt.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_provider.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_responses.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_serialize.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_serialize_pydantic_v2_compliance.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_spark.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_task.py +0 -0
- {openaivec-1.0.10 → openaivec-1.0.12}/tests/test_util.py +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
id-token: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
build-and-publish:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
environment: pypi
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout repository
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Install uv
|
|
22
|
+
uses: astral-sh/setup-uv@v7
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install 3.10
|
|
26
|
+
|
|
27
|
+
- name: Install dependencies via uv
|
|
28
|
+
run: uv sync --all-extras --dev
|
|
29
|
+
|
|
30
|
+
- name: Build with uv
|
|
31
|
+
run: uv build
|
|
32
|
+
|
|
33
|
+
- name: Install cosign
|
|
34
|
+
uses: sigstore/cosign-installer@v3
|
|
35
|
+
|
|
36
|
+
- name: Sign artifacts with cosign (OIDC)
|
|
37
|
+
run: |
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
ls -la dist
|
|
40
|
+
mkdir -p signatures
|
|
41
|
+
for f in dist/*; do
|
|
42
|
+
if [ -f "$f" ]; then
|
|
43
|
+
base="$(basename "$f")"
|
|
44
|
+
cosign sign-blob --yes \
|
|
45
|
+
--output-signature "signatures/${base}.sig" \
|
|
46
|
+
--output-certificate "signatures/${base}.pem" \
|
|
47
|
+
"$f"
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
- name: Create GitHub Release
|
|
52
|
+
uses: softprops/action-gh-release@v2
|
|
53
|
+
with:
|
|
54
|
+
files: |
|
|
55
|
+
dist/*
|
|
56
|
+
signatures/*
|
|
57
|
+
|
|
58
|
+
- name: Publish to PyPI
|
|
59
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -6,6 +6,7 @@ on:
|
|
|
6
6
|
|
|
7
7
|
permissions:
|
|
8
8
|
contents: read
|
|
9
|
+
issues: write
|
|
9
10
|
pull-requests: read
|
|
10
11
|
|
|
11
12
|
jobs:
|
|
@@ -23,6 +24,19 @@ jobs:
|
|
|
23
24
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
24
25
|
|
|
25
26
|
steps:
|
|
27
|
+
- name: Comment workflow run link
|
|
28
|
+
shell: bash
|
|
29
|
+
run: |
|
|
30
|
+
set -euo pipefail
|
|
31
|
+
COMMENTS_URL="${{ github.event.issue.comments_url }}"
|
|
32
|
+
RUN_URL="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
|
33
|
+
export RUN_URL
|
|
34
|
+
PAYLOAD="$(python -c 'import json,os; print(json.dumps({\"body\": f\"Integration workflow started: {os.environ[\\\"RUN_URL\\\"]}\"}))')"
|
|
35
|
+
curl -sS -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
|
36
|
+
-H "Accept: application/vnd.github+json" \
|
|
37
|
+
-d "$PAYLOAD" \
|
|
38
|
+
"$COMMENTS_URL"
|
|
39
|
+
|
|
26
40
|
- name: Fetch PR head SHA
|
|
27
41
|
id: pr
|
|
28
42
|
shell: bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: openaivec
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.12
|
|
4
4
|
Summary: Generative mutation for tabular calculation
|
|
5
5
|
Project-URL: Homepage, https://microsoft.github.io/openaivec/
|
|
6
6
|
Project-URL: Repository, https://github.com/microsoft/openaivec
|
|
@@ -63,7 +63,7 @@ print(sentiment.tolist())
|
|
|
63
63
|
# Output: ['Positive sentiment', 'Negative sentiment']
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
**
|
|
66
|
+
**Pandas tutorial (GitHub Pages):** https://microsoft.github.io/openaivec/examples/pandas/
|
|
67
67
|
|
|
68
68
|
## Benchmarks
|
|
69
69
|
|
|
@@ -82,6 +82,7 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
82
82
|
## Contents
|
|
83
83
|
|
|
84
84
|
- [Why openaivec?](#why-openaivec)
|
|
85
|
+
- [Overview](#overview)
|
|
85
86
|
- [Core Workflows](#core-workflows)
|
|
86
87
|
- [Using with Apache Spark UDFs](#using-with-apache-spark-udfs)
|
|
87
88
|
- [Building Prompts](#building-prompts)
|
|
@@ -93,14 +94,13 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
93
94
|
## Why openaivec?
|
|
94
95
|
|
|
95
96
|
- Drop-in `.ai` and `.aio` accessors keep pandas analysts in familiar tooling.
|
|
96
|
-
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, and
|
|
97
|
-
- Smart batching (`BatchingMapProxy`/`AsyncBatchingMapProxy`) dedupes prompts, preserves order, and releases waiters on failure.
|
|
97
|
+
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, preserve order, and release waiters on failure.
|
|
98
98
|
- Reasoning support mirrors the OpenAI SDK; structured outputs accept Pydantic `response_format`.
|
|
99
99
|
- Built-in caches and retries remove boilerplate; helpers reuse caches across pandas, Spark, and async flows.
|
|
100
100
|
- Spark UDFs and Microsoft Fabric guides move notebooks into production-scale ETL.
|
|
101
101
|
- Prompt tooling (`FewShotPromptBuilder`, `improve`) and the task library ship curated prompts with validated outputs.
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
## Overview
|
|
104
104
|
|
|
105
105
|
Vectorized OpenAI batch processing so you handle many inputs per call instead of one-by-one. Batching proxies dedupe inputs, enforce ordered outputs, and unblock waiters even on upstream errors. Cache helpers (`responses_with_cache`, Spark UDF builders) plug into the same layer so expensive prompts are reused across pandas, Spark, and async flows. Reasoning models honor SDK semantics. Requires Python 3.10+.
|
|
106
106
|
|
|
@@ -186,7 +186,7 @@ result = df.assign(
|
|
|
186
186
|
|
|
187
187
|
### Using with reasoning models
|
|
188
188
|
|
|
189
|
-
Reasoning models (o1-preview, o1-mini, o3-mini, etc.)
|
|
189
|
+
Reasoning models (o1-preview, o1-mini, o3-mini, etc.) follow OpenAI SDK semantics. Pass `reasoning` when you want to override model defaults.
|
|
190
190
|
|
|
191
191
|
```python
|
|
192
192
|
pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
@@ -194,7 +194,7 @@ pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
|
194
194
|
result = df.assign(
|
|
195
195
|
analysis=lambda df: df.text.ai.responses(
|
|
196
196
|
"Analyze this text step by step",
|
|
197
|
-
reasoning={"effort": "none"} # Optional: mirrors the OpenAI SDK argument
|
|
197
|
+
reasoning={"effort": "none"}, # Optional: mirrors the OpenAI SDK argument
|
|
198
198
|
)
|
|
199
199
|
)
|
|
200
200
|
```
|
|
@@ -254,7 +254,7 @@ df = pd.DataFrame({"text": [
|
|
|
254
254
|
async def process_data():
|
|
255
255
|
return await df["text"].aio.responses(
|
|
256
256
|
"Analyze sentiment and classify as positive/negative/neutral",
|
|
257
|
-
reasoning={"effort": "none"}, #
|
|
257
|
+
reasoning={"effort": "none"}, # Recommended for reasoning models
|
|
258
258
|
max_concurrency=12 # Allow up to 12 concurrent requests
|
|
259
259
|
)
|
|
260
260
|
|
|
@@ -37,7 +37,7 @@ print(sentiment.tolist())
|
|
|
37
37
|
# Output: ['Positive sentiment', 'Negative sentiment']
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
**
|
|
40
|
+
**Pandas tutorial (GitHub Pages):** https://microsoft.github.io/openaivec/examples/pandas/
|
|
41
41
|
|
|
42
42
|
## Benchmarks
|
|
43
43
|
|
|
@@ -56,6 +56,7 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
56
56
|
## Contents
|
|
57
57
|
|
|
58
58
|
- [Why openaivec?](#why-openaivec)
|
|
59
|
+
- [Overview](#overview)
|
|
59
60
|
- [Core Workflows](#core-workflows)
|
|
60
61
|
- [Using with Apache Spark UDFs](#using-with-apache-spark-udfs)
|
|
61
62
|
- [Building Prompts](#building-prompts)
|
|
@@ -67,14 +68,13 @@ Batching alone removes most HTTP overhead, and letting batching overlap with con
|
|
|
67
68
|
## Why openaivec?
|
|
68
69
|
|
|
69
70
|
- Drop-in `.ai` and `.aio` accessors keep pandas analysts in familiar tooling.
|
|
70
|
-
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, and
|
|
71
|
-
- Smart batching (`BatchingMapProxy`/`AsyncBatchingMapProxy`) dedupes prompts, preserves order, and releases waiters on failure.
|
|
71
|
+
- OpenAI batch-optimized: `BatchingMapProxy`/`AsyncBatchingMapProxy` coalesce requests, dedupe prompts, preserve order, and release waiters on failure.
|
|
72
72
|
- Reasoning support mirrors the OpenAI SDK; structured outputs accept Pydantic `response_format`.
|
|
73
73
|
- Built-in caches and retries remove boilerplate; helpers reuse caches across pandas, Spark, and async flows.
|
|
74
74
|
- Spark UDFs and Microsoft Fabric guides move notebooks into production-scale ETL.
|
|
75
75
|
- Prompt tooling (`FewShotPromptBuilder`, `improve`) and the task library ship curated prompts with validated outputs.
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
## Overview
|
|
78
78
|
|
|
79
79
|
Vectorized OpenAI batch processing so you handle many inputs per call instead of one-by-one. Batching proxies dedupe inputs, enforce ordered outputs, and unblock waiters even on upstream errors. Cache helpers (`responses_with_cache`, Spark UDF builders) plug into the same layer so expensive prompts are reused across pandas, Spark, and async flows. Reasoning models honor SDK semantics. Requires Python 3.10+.
|
|
80
80
|
|
|
@@ -160,7 +160,7 @@ result = df.assign(
|
|
|
160
160
|
|
|
161
161
|
### Using with reasoning models
|
|
162
162
|
|
|
163
|
-
Reasoning models (o1-preview, o1-mini, o3-mini, etc.)
|
|
163
|
+
Reasoning models (o1-preview, o1-mini, o3-mini, etc.) follow OpenAI SDK semantics. Pass `reasoning` when you want to override model defaults.
|
|
164
164
|
|
|
165
165
|
```python
|
|
166
166
|
pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
@@ -168,7 +168,7 @@ pandas_ext.set_responses_model("o1-mini") # Set your reasoning model
|
|
|
168
168
|
result = df.assign(
|
|
169
169
|
analysis=lambda df: df.text.ai.responses(
|
|
170
170
|
"Analyze this text step by step",
|
|
171
|
-
reasoning={"effort": "none"} # Optional: mirrors the OpenAI SDK argument
|
|
171
|
+
reasoning={"effort": "none"}, # Optional: mirrors the OpenAI SDK argument
|
|
172
172
|
)
|
|
173
173
|
)
|
|
174
174
|
```
|
|
@@ -228,7 +228,7 @@ df = pd.DataFrame({"text": [
|
|
|
228
228
|
async def process_data():
|
|
229
229
|
return await df["text"].aio.responses(
|
|
230
230
|
"Analyze sentiment and classify as positive/negative/neutral",
|
|
231
|
-
reasoning={"effort": "none"}, #
|
|
231
|
+
reasoning={"effort": "none"}, # Recommended for reasoning models
|
|
232
232
|
max_concurrency=12 # Allow up to 12 concurrent requests
|
|
233
233
|
)
|
|
234
234
|
|
|
@@ -21,7 +21,10 @@ class BatchSizeSuggester:
|
|
|
21
21
|
min_batch_size: int = 10
|
|
22
22
|
min_duration: float = 30.0
|
|
23
23
|
max_duration: float = 60.0
|
|
24
|
-
|
|
24
|
+
step_ratio_up: float = 0.1
|
|
25
|
+
step_ratio_down: float = 0.2
|
|
26
|
+
max_step: int | None = None
|
|
27
|
+
min_step: int = 1
|
|
25
28
|
sample_size: int = 4
|
|
26
29
|
_history: list[PerformanceMetric] = field(default_factory=list)
|
|
27
30
|
_lock: threading.RLock = field(default_factory=threading.RLock, repr=False)
|
|
@@ -34,8 +37,14 @@ class BatchSizeSuggester:
|
|
|
34
37
|
raise ValueError("current_batch_size must be >= min_batch_size")
|
|
35
38
|
if self.sample_size <= 0:
|
|
36
39
|
raise ValueError("sample_size must be > 0")
|
|
37
|
-
if self.
|
|
38
|
-
raise ValueError("
|
|
40
|
+
if self.step_ratio_up <= 0:
|
|
41
|
+
raise ValueError("step_ratio_up must be > 0")
|
|
42
|
+
if self.step_ratio_down <= 0:
|
|
43
|
+
raise ValueError("step_ratio_down must be > 0")
|
|
44
|
+
if self.max_step is not None and self.max_step <= 0:
|
|
45
|
+
raise ValueError("max_step must be > 0")
|
|
46
|
+
if self.min_step <= 0:
|
|
47
|
+
raise ValueError("min_step must be > 0")
|
|
39
48
|
if self.min_duration <= 0 or self.max_duration <= 0:
|
|
40
49
|
raise ValueError("min_duration and max_duration must be > 0")
|
|
41
50
|
if self.min_duration >= self.max_duration:
|
|
@@ -94,9 +103,15 @@ class BatchSizeSuggester:
|
|
|
94
103
|
current_size = self.current_batch_size
|
|
95
104
|
|
|
96
105
|
if average_duration < self.min_duration:
|
|
97
|
-
|
|
106
|
+
delta = max(self.min_step, int(current_size * self.step_ratio_up))
|
|
107
|
+
if self.max_step is not None:
|
|
108
|
+
delta = min(delta, self.max_step)
|
|
109
|
+
new_batch_size = current_size + delta
|
|
98
110
|
elif average_duration > self.max_duration:
|
|
99
|
-
|
|
111
|
+
delta = max(self.min_step, int(current_size * self.step_ratio_down))
|
|
112
|
+
if self.max_step is not None:
|
|
113
|
+
delta = min(delta, self.max_step)
|
|
114
|
+
new_batch_size = current_size - delta
|
|
100
115
|
else:
|
|
101
116
|
new_batch_size = current_size
|
|
102
117
|
|
|
@@ -33,21 +33,35 @@ class TestBatchSizeSuggester:
|
|
|
33
33
|
assert suggester.min_batch_size == 10
|
|
34
34
|
assert suggester.min_duration == 30.0
|
|
35
35
|
assert suggester.max_duration == 60.0
|
|
36
|
-
assert suggester.
|
|
36
|
+
assert suggester.step_ratio_up == 0.1
|
|
37
|
+
assert suggester.step_ratio_down == 0.2
|
|
38
|
+
assert suggester.max_step is None
|
|
39
|
+
assert suggester.min_step == 1
|
|
37
40
|
assert suggester.sample_size == 4
|
|
38
41
|
assert len(suggester._history) == 0
|
|
39
42
|
assert suggester._batch_size_changed_at is None
|
|
40
43
|
|
|
41
44
|
def test_custom_initialization(self):
|
|
42
45
|
suggester = BatchSizeSuggester(
|
|
43
|
-
current_batch_size=20,
|
|
46
|
+
current_batch_size=20,
|
|
47
|
+
min_batch_size=5,
|
|
48
|
+
min_duration=15.0,
|
|
49
|
+
max_duration=45.0,
|
|
50
|
+
step_ratio_up=0.15,
|
|
51
|
+
step_ratio_down=0.25,
|
|
52
|
+
min_step=2,
|
|
53
|
+
max_step=5,
|
|
54
|
+
sample_size=5,
|
|
44
55
|
)
|
|
45
56
|
|
|
46
57
|
assert suggester.current_batch_size == 20
|
|
47
58
|
assert suggester.min_batch_size == 5
|
|
48
59
|
assert suggester.min_duration == 15.0
|
|
49
60
|
assert suggester.max_duration == 45.0
|
|
50
|
-
assert suggester.
|
|
61
|
+
assert suggester.step_ratio_up == 0.15
|
|
62
|
+
assert suggester.step_ratio_down == 0.25
|
|
63
|
+
assert suggester.min_step == 2
|
|
64
|
+
assert suggester.max_step == 5
|
|
51
65
|
assert suggester.sample_size == 5
|
|
52
66
|
|
|
53
67
|
@pytest.mark.parametrize(
|
|
@@ -56,7 +70,10 @@ class TestBatchSizeSuggester:
|
|
|
56
70
|
({"min_batch_size": 0}, "min_batch_size must be > 0"),
|
|
57
71
|
({"current_batch_size": 5, "min_batch_size": 10}, "current_batch_size must be >= min_batch_size"),
|
|
58
72
|
({"sample_size": 0}, "sample_size must be > 0"),
|
|
59
|
-
({"
|
|
73
|
+
({"step_ratio_up": 0}, "step_ratio_up must be > 0"),
|
|
74
|
+
({"step_ratio_down": 0}, "step_ratio_down must be > 0"),
|
|
75
|
+
({"min_step": 0}, "min_step must be > 0"),
|
|
76
|
+
({"max_step": 0}, "max_step must be > 0"),
|
|
60
77
|
({"min_duration": 0}, "min_duration and max_duration must be > 0"),
|
|
61
78
|
({"max_duration": 0}, "min_duration and max_duration must be > 0"),
|
|
62
79
|
({"min_duration": 60, "max_duration": 30}, "min_duration must be < max_duration"),
|
|
@@ -190,7 +207,8 @@ class TestBatchSizeSuggester:
|
|
|
190
207
|
min_batch_size=min_batch_size,
|
|
191
208
|
min_duration=0.5, # 0.5 seconds (test scale)
|
|
192
209
|
max_duration=1.0, # 1.0 seconds (test scale)
|
|
193
|
-
|
|
210
|
+
step_ratio_up=0.1,
|
|
211
|
+
step_ratio_down=0.1,
|
|
194
212
|
sample_size=3,
|
|
195
213
|
)
|
|
196
214
|
|
|
@@ -214,7 +232,7 @@ class TestBatchSizeSuggester:
|
|
|
214
232
|
min_batch_size=5,
|
|
215
233
|
min_duration=0.5, # 0.5 seconds (test scale)
|
|
216
234
|
max_duration=1.0, # 1.0 seconds (test scale)
|
|
217
|
-
|
|
235
|
+
step_ratio_down=0.5, # Large step to force below minimum
|
|
218
236
|
sample_size=3,
|
|
219
237
|
)
|
|
220
238
|
|
|
@@ -227,6 +245,60 @@ class TestBatchSizeSuggester:
|
|
|
227
245
|
assert new_size == 5 # Should not go below min_batch_size
|
|
228
246
|
assert suggester.current_batch_size == 5
|
|
229
247
|
|
|
248
|
+
def test_min_step_enforces_minimum_delta_on_increase(self):
|
|
249
|
+
suggester = BatchSizeSuggester(
|
|
250
|
+
current_batch_size=10,
|
|
251
|
+
min_batch_size=1,
|
|
252
|
+
min_duration=0.5,
|
|
253
|
+
max_duration=1.0,
|
|
254
|
+
step_ratio_up=0.1, # 10 * 0.1 = 1
|
|
255
|
+
min_step=5, # force delta to 5
|
|
256
|
+
sample_size=3,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
for i in range(3):
|
|
260
|
+
with suggester.record(batch_size=10):
|
|
261
|
+
time.sleep(0.1) # fast -> increase
|
|
262
|
+
|
|
263
|
+
new_size = suggester.suggest_batch_size()
|
|
264
|
+
assert new_size == 15
|
|
265
|
+
|
|
266
|
+
def test_min_step_enforces_minimum_delta_on_decrease(self):
|
|
267
|
+
suggester = BatchSizeSuggester(
|
|
268
|
+
current_batch_size=20,
|
|
269
|
+
min_batch_size=1,
|
|
270
|
+
min_duration=0.5,
|
|
271
|
+
max_duration=1.0,
|
|
272
|
+
step_ratio_down=0.1, # 20 * 0.1 = 2
|
|
273
|
+
min_step=5, # force delta to 5
|
|
274
|
+
sample_size=3,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
for i in range(3):
|
|
278
|
+
with suggester.record(batch_size=20):
|
|
279
|
+
time.sleep(1.5) # slow -> decrease
|
|
280
|
+
|
|
281
|
+
new_size = suggester.suggest_batch_size()
|
|
282
|
+
assert new_size == 15
|
|
283
|
+
|
|
284
|
+
def test_max_step_caps_delta(self):
|
|
285
|
+
suggester = BatchSizeSuggester(
|
|
286
|
+
current_batch_size=50,
|
|
287
|
+
min_batch_size=1,
|
|
288
|
+
min_duration=0.5,
|
|
289
|
+
max_duration=1.0,
|
|
290
|
+
step_ratio_up=0.5, # 50 * 0.5 = 25
|
|
291
|
+
max_step=10, # cap delta at 10
|
|
292
|
+
sample_size=3,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
for i in range(3):
|
|
296
|
+
with suggester.record(batch_size=50):
|
|
297
|
+
time.sleep(0.1) # fast -> increase
|
|
298
|
+
|
|
299
|
+
new_size = suggester.suggest_batch_size()
|
|
300
|
+
assert new_size == 60
|
|
301
|
+
|
|
230
302
|
def test_thread_safety(self):
|
|
231
303
|
suggester = BatchSizeSuggester(sample_size=10)
|
|
232
304
|
results = []
|
|
@@ -42,7 +42,8 @@ def test_sync_proxy_suggester_adapts_batch_size():
|
|
|
42
42
|
proxy.suggester.min_duration = 0.001 # 1ms
|
|
43
43
|
proxy.suggester.max_duration = 0.002 # 2ms
|
|
44
44
|
proxy.suggester.sample_size = 2
|
|
45
|
-
proxy.suggester.
|
|
45
|
+
proxy.suggester.step_ratio_up = 0.5
|
|
46
|
+
proxy.suggester.step_ratio_down = 0.5
|
|
46
47
|
|
|
47
48
|
import time
|
|
48
49
|
|