From 80faef553b6c6ad5174c76b0398689bb3d63df08 Mon Sep 17 00:00:00 2001 From: Xavier Daull Date: Thu, 28 Nov 2024 17:44:02 +0100 Subject: [PATCH 1/4] a subclassed optimizer from OptoPrime to allow multiple candidates per iterations per step to allow to derive different strategies for candidate generation and selection --- ...st_time_loss_for_code_OptoPrimeMulti.ipynb | 600 ++++++++++++++++++ opto/optimizers/__init__.py | 3 +- opto/optimizers/optoprimemulti.py | 145 +++++ 3 files changed, 747 insertions(+), 1 deletion(-) create mode 100644 examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb create mode 100644 opto/optimizers/optoprimemulti.py diff --git a/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb new file mode 100644 index 0000000..a5881d0 --- /dev/null +++ b/examples/textgrad_examples/notebooks/textgrad_test_time_loss_for_code_OptoPrimeMulti.ipynb @@ -0,0 +1,600 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "-WGqLq5vb7Jm", + "outputId": "f18bba80-41ae-4473-f426-4fb8f5746082" + }, + "outputs": [], + "source": [ + "%pip install textgrad\n", + "%pip install git+https://github.com/microsoft/Trace.git\n", + "%pip install dask[dataframe]\n", + "%pip install autogen" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7MTXRbDhcHAP" + }, + "outputs": [], + "source": [ + "import os\n", + "import openai\n", + "\n", + "os.environ['OPENAI_API_KEY'] = \"\"\n", + "\n", + "OAI_CONFIG_LIST = [ { \"model\": \"gpt-4o-mini\", \"api_key\": os.environ['OPENAI_API_KEY'],}]\n", + "\n", + "import json; config_file_path = \"/content/config_list.json\"; json.dump(OAI_CONFIG_LIST, open(config_file_path, \"w\")); os.environ['OAI_CONFIG_LIST'] = config_file_path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "JftiVF1eb0rH" + }, + "outputs": [], + "source": [ + "from opto import trace\n", + "from opto.optimizers import OptoPrime, OptoPrimeMulti\n", + "\n", + "import random\n", + "import time" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "id": "cO6nE2LPb0rH" + }, + "outputs": [], + "source": [ + "# We'll use below utilities to run a python function.\n", + "from IPython.core.interactiveshell import InteractiveShell\n", + "\n", + "def run_function_in_interpreter(func_code):\n", + " # raise Exception(\"This function will run the code returned by GPT-4o. Remove this if you'd like to run the code!\")\n", + " interpreter = InteractiveShell.instance()\n", + "\n", + " interpreter.run_cell(func_code, store_history=False, silent=True)\n", + "\n", + " func_name = func_code.split(\"def \")[1].split(\"(\")[0].strip()\n", + " func = interpreter.user_ns[func_name]\n", + "\n", + " return func\n", + "\n", + "\n", + "\n", + "def test_longest_increasing_subsequence(fn):\n", + " nums = [10, 22, 9, 33, 21, 50, 41, 60]\n", + " assert fn(nums) == 5\n", + "\n", + " nums = [7, 2, 1, 3, 8, 4, 9, 6, 5]\n", + " assert fn(nums) == 4\n", + "\n", + " nums = [5, 4, 3, 2, 1]\n", + " assert fn(nums) == 1\n", + "\n", + " nums = [1, 2, 3, 4, 5]\n", + " assert fn(nums) == 5\n", + "\n", + " nums = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5]\n", + " assert fn(nums) == 4\n", + "\n", + " nums = [10, 9, 2, 5, 3, 7, 101, 18]\n", + " assert fn(nums) == 4\n", + "\n", + " nums = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15]\n", + " assert fn(nums) == 6\n", + "\n", + " nums = [7, 7, 7, 7, 7, 7, 7]\n", + " assert fn(nums) == 1\n", + "\n", + " nums = [20, 25, 47, 35, 56, 68, 98, 101, 212, 301, 415, 500]\n", + " assert fn(nums) == 11\n", + "\n", + " nums = [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]\n", + " assert fn(nums) == 1\n", + "\n", + " print(\"All test cases passed!\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": { + "id": "OYkVBYtkb0rH" + }, + "outputs": [], + "source": [ + "problem_text = \"\"\"Longest Increasing Subsequence (LIS)\n", + "\n", + "Problem Statement:\n", + "Given a sequence of integers, find the length of the longest subsequence that is strictly increasing. A subsequence is a sequence that can be derived from another sequence by deleting some or no elements without changing the order of the remaining elements.\n", + "\n", + "Input:\n", + "The input consists of a list of integers representing the sequence.\n", + "\n", + "Output:\n", + "The output should be an integer representing the length of the longest increasing subsequence.\"\"\"\n", + "\n", + "initial_solution = \"\"\"\n", + "def longest_increasing_subsequence(nums):\n", + " n = len(nums)\n", + " dp = [1] * n\n", + "\n", + " for i in range(1, n):\n", + " for j in range(i):\n", + " if nums[i] > nums[j]:\n", + " dp[i] = max(dp[i], dp[j] + 1)\n", + "\n", + " max_length = max(dp)\n", + " lis = []\n", + "\n", + " for i in range(n - 1, -1, -1):\n", + " if dp[i] == max_length:\n", + " lis.append(nums[i])\n", + " max_length -= 1\n", + "\n", + " return len(lis[::-1])\n", + "\"\"\"\n", + "\n", + "# Generate a random test case\n", + "def generate_random_test_case(size, min_value, max_value):\n", + " return [random.randint(min_value, max_value) for _ in range(size)]\n", + "\n", + "# Test the function with a random test case\n", + "size = 10000 # Adjust the size as needed\n", + "min_value = 1\n", + "max_value = 1000\n", + "\n", + "nums = generate_random_test_case(size, min_value, max_value)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "fqTOVqftb0rI", + "outputId": "a7506eb0-7cc0-47c7-facc-76f93541117e" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Test Case Size: 10000\n", + "Longest Increasing Subsequence Length: 176\n", + "Runtime: 12.51021 seconds\n", + "All test cases passed!\n" + ] + } + ], + "source": [ + "longest_increasing_subsequence = run_function_in_interpreter(initial_solution)\n", + "\n", + "start_time = time.time()\n", + "lis = longest_increasing_subsequence(nums)\n", + "end_time = time.time()\n", + "\n", + "print(f\"Test Case Size: {size}\")\n", + "print(f\"Longest Increasing Subsequence Length: {lis}\")\n", + "print(f\"Runtime: {end_time - start_time:.5f} seconds\")\n", + "\n", + "# Test for all test cases\n", + "test_longest_increasing_subsequence(longest_increasing_subsequence)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Tj_xi5Jib0rI" + }, + "source": [ + "# Trace code" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 645 + }, + "id": "wSgBz-Amb0rI", + "outputId": "299e048c-d0db-4b9c-c557-f72fd962d4a2" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\n", + "PYDEV DEBUGGER WARNING:\n", + "sys.settrace() should not be used when the debugger is being used.\n", + "This may cause the debugger to stop working correctly.\n", + "If this is needed, please check: \n", + "http://pydev.blogspot.com/2007/06/why-cant-pydev-debugger-work-with.html\n", + "to see how to restore the debug tracing back correctly.\n", + "Call Location:\n", + " File \"/usr/local/lib/python3.10/dist-packages/opto/trace/bundle.py\", line 359, in sync_call_fun\n", + " sys.settrace(oldtracer)\n", + "\n" + ] + }, + { + "data": { + "image/svg+xml": [ + "\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "%3\n", + "\n", + "\n", + "\n", + "x0\n", + "\n", + "x0\n", + "[Node] This is a node in a computational graph.\n", + "Problem: {problem_text}\n", + "Current Code: {solution}\n", + "\n", + "\n", + "\n", + "format0\n", + "\n", + "format0\n", + "[format] Fills in a string template with content, str.format(). .\n", + "Problem: Longest Increasing Subsequence (LIS)\n", + "Problem Statement:\n", + "Given a sequence of integers, find...\n", + "\n", + "\n", + "\n", + "x0->format0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "problem_text0\n", + "\n", + "problem_text0\n", + "[Node] This is a node in a computational graph.\n", + "Longest Increasing Subsequence (LIS)\n", + "Problem Statement:\n", + "Given a sequence of integers, find the leng...\n", + "\n", + "\n", + "\n", + "problem_text0->format0\n", + "\n", + "\n", + "\n", + "\n", + "\n", + "str0\n", + "\n", + "str0\n", + "[ParameterNode] This is a ParameterNode in a computational graph.\n", + "def longest_increasing_subsequence(nums):\n", + "    n = len(nums)\n", + "    dp = [1] * n\n", + "    for i in range(1,...\n", + "\n", + "\n", + "\n", + "str0->format0\n", + "\n", + "\n", + "\n", + "\n", + "\n" + ], + "text/plain": [ + "" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\n", + "code = trace.node(initial_solution, trainable=True)\n", + "opt = OptoPrimeMulti([code])\n", + "\n", + "feedback = \"Think about the problem and the code snippet. Does the code solve the problem? What is the runtime complexity? Improve the runtime complexity of the code.\"\n", + "format_string = \"Problem: {problem_text}\\nCurrent Code: {solution}\"\n", + "\n", + "from opto.trace import operators as ops\n", + "problem = ops.format(format_string, problem_text=problem_text, solution=code)\n", + "opt.zero_feedback()\n", + "\n", + "# Let's visualize our computation graph.\n", + "problem.backward(feedback, visualize=True)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "BFgB5Ngfb0rJ", + "outputId": "6f5c2f0f-693a-4954-9898-acd59addaf08" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Temperatures for responses: [1.3, 0.9750000000000001, 0.65, 0.32499999999999996, 0.0]\n", + "LLM responses:\n", + " ['{\\n\"reasoning\": \"The #Instruction asks for changes in #Variables based on #Feedback about improving the output. The #Feedback indicates that while the current code solves the problem of finding the length of the longest increasing subsequence, the runtime complexity of the algorithm is O(n^2), which can be optimized to O(n log n) using binary search. Therefore, the suggested improvement involves changing `str0` to implement a more efficient version of the longest increasing subsequence algorithm. A common approach is to utilize a list to track the smallest tail for all subsequences of a given length.\",\\n\"answer\": \"\",\\n\"suggestion\": {\\n \"str0\": \"def longest_increasing_subsequence(nums):\\\\n from bisect import bisect_left\\\\n if not nums:\\\\n return 0\\\\n lis = []\\\\n for x in nums:\\\\n i = bisect_left(lis, x)\\\\n if i == len(lis):\\\\n lis.append(x)\\\\n else:\\\\n lis[i] = x\\\\n return len(lis)\"\\n}\\n}']\n", + "LLM responses:\n", + " ['{\\n\"reasoning\": \"The instruction asks to improve the output based on the feedback. The feedback indicates that while the provided code for finding the longest increasing subsequence (LIS) works, its runtime complexity can be enhanced. The current solution has a time complexity of O(n^2), which can be improved to O(n log n) using a more efficient algorithm such as binary search with a dynamic array. This involves using a list to keep track of the smallest tail of all increasing subsequences of different lengths and applying binary search to maintain this list when inserting new elements. Therefore, I suggest changing the implementation of the function `str0` to include this optimized approach. The expected result is a new version of the LIS function that operates with better efficiency.\",\\n\"answer\": \"The updated code for the longest increasing subsequence implementation should look like this:\\\\ndef longest_increasing_subsequence(nums):\\\\n from bisect import bisect_left\\\\n lis = []\\\\n for num in nums:\\\\n pos = bisect_left(lis, num)\\\\n if pos == len(lis):\\\\n lis.append(num)\\\\n else:\\\\n lis[pos] = num\\\\n return len(lis)\",\\n\"suggestion\": {\\n \"str0\": \"def longest_increasing_subsequence(nums):\\\\n from bisect import bisect_left\\\\n lis = []\\\\n for num in nums:\\\\n pos = bisect_left(lis, num)\\\\n if pos == len(lis):\\\\n lis.append(num)\\\\n else:\\\\n lis[pos] = num\\\\n return len(lis)\"\\n}}\\n']\n", + "LLM responses:\n", + " ['{\\n\"reasoning\": \"The instruction asks to improve the output based on the feedback provided. The feedback suggests that while the current code does solve the problem of finding the length of the longest increasing subsequence, its runtime complexity can be improved. The current code has a time complexity of O(n^2) due to the nested loops used for comparing elements. A more efficient approach would be to use a binary search algorithm that can reduce the complexity to O(n log n). Therefore, I need to suggest a change in the implementation of the function \\'longest_increasing_subsequence\\' to improve its runtime complexity.\",\\n\"answer\": \"The improved solution should use a dynamic programming approach with binary search.\",\\n\"suggestion\": {\\n \"str0\": \"def longest_increasing_subsequence(nums):\\\\n from bisect import bisect_left\\\\n subsequence = []\\\\n for num in nums:\\\\n pos = bisect_left(subsequence, num)\\\\n if pos == len(subsequence):\\\\n subsequence.append(num)\\\\n else:\\\\n subsequence[pos] = num\\\\n return len(subsequence)\"\\n}\\n}']\n", + "LLM responses:\n", + " ['{\\n\"reasoning\": \"The instruction asks to improve the output based on the feedback, which suggests that while the current code solves the problem of finding the length of the longest increasing subsequence (LIS), it could be optimized for better runtime complexity. The current implementation has a time complexity of O(n^2) due to the nested loops. To improve this, we can implement a more efficient algorithm that uses binary search, reducing the time complexity to O(n log n). The suggested changes involve modifying the `str0` variable to include a new implementation of the LIS algorithm that utilizes this optimized approach. This change is expected to enhance the performance of the code without altering its functionality.\", \\n\"answer\": \"The current implementation of the longest increasing subsequence function can be improved for better runtime complexity.\", \\n\"suggestion\": {\\n \"str0\": \"def longest_increasing_subsequence(nums):\\\\n from bisect import bisect_left\\\\n lis = []\\\\n for num in nums:\\\\n pos = bisect_left(lis, num)\\\\n if pos == len(lis):\\\\n lis.append(num)\\\\n else:\\\\n lis[pos] = num\\\\n return len(lis)\"\\n}}\\n']\n", + "LLM responses:\n", + " ['{\\n\"reasoning\": \"The instruction asks to improve the output based on the feedback provided. The feedback suggests that while the code solves the problem of finding the longest increasing subsequence (LIS), it has a runtime complexity of O(n^2) due to the nested loops. To improve the runtime complexity, we can implement a more efficient algorithm that uses binary search, which can reduce the complexity to O(n log n). Therefore, I will suggest a new implementation for the function \\'longest_increasing_subsequence\\' that utilizes binary search to achieve this improved performance.\",\\n\"answer\": \"The current code does solve the problem of finding the longest increasing subsequence, but it can be optimized for better performance.\",\\n\"suggestion\": {\\n \"str0\": \"def longest_increasing_subsequence(nums):\\\\n from bisect import bisect_left\\\\n lis = []\\\\n for num in nums:\\\\n pos = bisect_left(lis, num)\\\\n if pos == len(lis):\\\\n lis.append(num)\\\\n else:\\\\n lis[pos] = num\\\\n return len(lis)\"\\n}\\n}']\n" + ] + } + ], + "source": [ + "# Let's update the code\n", + "opt.step(verbose='output')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "h9Peebbkc5Mv", + "outputId": "31a2bc3c-6835-444b-b42e-aadfcb61e6be" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "{: 'def longest_increasing_subsequence(nums):\\n from bisect import bisect_left\\n if not nums:\\n return 0\\n lis = []\\n for x in nums:\\n i = bisect_left(lis, x)\\n if i == len(lis):\\n lis.append(x)\\n else:\\n lis[i] = x\\n return len(lis)'}\n", + "\n", + "{: 'def longest_increasing_subsequence(nums):\\n from bisect import bisect_left\\n lis = []\\n for num in nums:\\n pos = bisect_left(lis, num)\\n if pos == len(lis):\\n lis.append(num)\\n else:\\n lis[pos] = num\\n return len(lis)'}\n", + "\n", + "{: 'def longest_increasing_subsequence(nums):\\n from bisect import bisect_left\\n subsequence = []\\n for num in nums:\\n pos = bisect_left(subsequence, num)\\n if pos == len(subsequence):\\n subsequence.append(num)\\n else:\\n subsequence[pos] = num\\n return len(subsequence)'}\n", + "\n", + "{: 'def longest_increasing_subsequence(nums):\\n from bisect import bisect_left\\n lis = []\\n for num in nums:\\n pos = bisect_left(lis, num)\\n if pos == len(lis):\\n lis.append(num)\\n else:\\n lis[pos] = num\\n return len(lis)'}\n", + "\n", + "{: 'def longest_increasing_subsequence(nums):\\n from bisect import bisect_left\\n lis = []\\n for num in nums:\\n pos = bisect_left(lis, num)\\n if pos == len(lis):\\n lis.append(num)\\n else:\\n lis[pos] = num\\n return len(lis)'}\n", + "\n" + ] + } + ], + "source": [ + "for c in opt.candidates:\n", + " print(f\"{c}\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "RC6AQahriWFc", + "outputId": "282e7f14-cf29-4f73-93a1-49de05ae837d" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Candidate 1:\n", + " Longest Increasing Subsequence Length: 176\n", + " Runtime: 0.00793 seconds\n", + " Code: <<>>\n", + "\n", + "Candidate 2:\n", + " Longest Increasing Subsequence Length: 176\n", + " Runtime: 0.01296 seconds\n", + " Code: <<>>\n", + "\n", + "Candidate 3:\n", + " Longest Increasing Subsequence Length: 176\n", + " Runtime: 0.01116 seconds\n", + " Code: <<>>\n", + "\n", + "Candidate 4:\n", + " Longest Increasing Subsequence Length: 176\n", + " Runtime: 0.01040 seconds\n", + " Code: <<>>\n", + "\n", + "Candidate 5:\n", + " Longest Increasing Subsequence Length: 176\n", + " Runtime: 0.01898 seconds\n", + " Code: <<>>\n", + "\n", + "Execution Summary:\n", + "Candidate 1: Result = 176, Runtime = 0.00793 seconds\n", + "Candidate 2: Result = 176, Runtime = 0.01296 seconds\n", + "Candidate 3: Result = 176, Runtime = 0.01116 seconds\n", + "Candidate 4: Result = 176, Runtime = 0.01040 seconds\n", + "Candidate 5: Result = 176, Runtime = 0.01898 seconds\n" + ] + } + ], + "source": [ + "# Test all candidates and log execution times\n", + "execution_results = []\n", + "\n", + "for i, candidate in enumerate(opt.candidates):\n", + " if not candidate: # Skip invalid candidates\n", + " print(f\"Candidate {i+1}: Skipped (Invalid)\")\n", + " continue\n", + "\n", + " # Extract the function code from the dictionary\n", + " func_code = list(candidate.values())[0] # Assumes there's only one key-value pair in the dictionary\n", + " if not func_code:\n", + " print(f\"Candidate {i+1}: No code found\")\n", + " continue\n", + "\n", + " # Compile and run the function\n", + " func = run_function_in_interpreter(func_code) # Extract and run candidate function\n", + " try:\n", + " start_time = time.time()\n", + " result = func(nums) # Test the function\n", + " end_time = time.time()\n", + "\n", + " runtime = end_time - start_time\n", + " execution_results.append({\n", + " \"candidate\": i + 1,\n", + " \"result\": result,\n", + " \"runtime\": runtime\n", + " })\n", + "\n", + " func_code_nonl = func_code.replace('\\n',' ')\n", + " print(f\"Candidate {i+1}:\")\n", + " print(f\" Longest Increasing Subsequence Length: {result}\")\n", + " print(f\" Runtime: {runtime:.5f} seconds\")\n", + " print(f\" Code: <<<{func_code_nonl}>>>\\n\")\n", + "\n", + " except Exception as e:\n", + " print(f\"Candidate {i+1}: Failed with error: {e}\\n\")\n", + "\n", + "# Display a summary of all candidate results\n", + "print(\"Execution Summary:\")\n", + "for res in execution_results:\n", + " print(f\"Candidate {res['candidate']}: Result = {res['result']}, Runtime = {res['runtime']:.5f} seconds\")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "J73bk2Ieb0rJ", + "outputId": "6b7d059c-03bc-4bae-f764-aaad8513693e" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Longest Increasing Subsequence Length: 176\n", + "Runtime: 0.00555 seconds\n", + "All test cases passed!\n" + ] + } + ], + "source": [ + "# Hopefully, we should get much better runtime!\n", + "longest_increasing_subsequence = run_function_in_interpreter(code.data)\n", + "\n", + "start_time = time.time()\n", + "lis = longest_increasing_subsequence(nums)\n", + "end_time = time.time()\n", + "\n", + "print(f\"Longest Increasing Subsequence Length: {lis}\")\n", + "print(f\"Runtime: {end_time - start_time:.5f} seconds\")\n", + "\n", + "test_longest_increasing_subsequence(longest_increasing_subsequence)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "oKL459B3b0rJ" + }, + "source": [ + "At this point, OptoPrime in Trace solves the problem. There's no need to further iterate." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "6wgWIOZ9b0rJ", + "outputId": "e8c91b2e-7dee-4254-911b-ad91b035a3d7" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "def longest_increasing_subsequence(nums):\n", + " from bisect import bisect_left\n", + " lis = []\n", + " for num in nums:\n", + " pos = bisect_left(lis, num)\n", + " if pos == len(lis):\n", + " lis.append(num)\n", + " else:\n", + " lis[pos] = num\n", + " return len(lis)\n" + ] + } + ], + "source": [ + "print(code.data)" + ] + } + ], + "metadata": { + "colab": { + "provenance": [] + }, + "kernelspec": { + "display_name": "trace-3.9", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.19" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} diff --git a/opto/optimizers/__init__.py b/opto/optimizers/__init__.py index 362eff5..74a0bdd 100644 --- a/opto/optimizers/__init__.py +++ b/opto/optimizers/__init__.py @@ -1,5 +1,6 @@ from opto.optimizers.optoprime import OptoPrime +from opto.optimizers.optoprimemulti import OptoPrimeMulti from opto.optimizers.opro import OPRO from opto.optimizers.textgrad import TextGrad -__all__ = ["OPRO", "OptoPrime", "TextGrad"] \ No newline at end of file +__all__ = ["OPRO", "OptoPrime", "OptoPrimeMulti", "TextGrad"] \ No newline at end of file diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py new file mode 100644 index 0000000..5f0c9c7 --- /dev/null +++ b/opto/optimizers/optoprimemulti.py @@ -0,0 +1,145 @@ +from typing import Any, List, Dict, Union, Tuple +import json +from textwrap import dedent + +from opto.trace.propagators import GraphPropagator +from opto.optimizers.optoprime import OptoPrime + +class OptoPrimeMulti(OptoPrime): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.candidates = [] # Store all candidate solutions + self.selected_candidate = None # Store the selected candidate solution + + def call_llm( + self, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, + max_tokens: int = 4096, num_responses: int = 1, temperature: float = 0. + ) -> List[str]: + """Call the LLM with a prompt and return multiple responses.""" + if verbose not in (False, "output"): + print("Prompt\n", system_prompt + user_prompt) + + messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}] + + try: + response = self.llm.create( + messages=messages, + response_format={"type": "json_object"}, + max_tokens=max_tokens, + n=num_responses, + temperature=temperature, + ) + except Exception as e: + if verbose: + print(f"ERROR {e}") + # Default to returning an empty response list if an error occurs # Error handling improvement + return [] + + responses = [choice.message.content for choice in response.choices] + + if verbose: + print("LLM responses:\n", responses) + return responses + + def generate_candidates( + self, summary, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, + mask=None, max_tokens: int = None, num_responses: int = 5, temperature_range: List[float] = [1.3, 0.] + ) -> List[str]: + """ + Generate multiple candidates with progressively decreasing temperatures. + Args: + summary: The summarized problem instance. + system_prompt (str): The system-level prompt. + user_prompt (str): The user-level prompt. + verbose (bool): Whether to print debug information. + mask: Mask for the problem instance. + max_tokens (int, optional): Maximum token limit for the LLM responses. + num_responses (int): Number of responses to request. + temperature_range (List[float]): [max_temperature, min_temperature]. + Returns: + List[str]: List of LLM responses as strings. + """ + max_tokens = max_tokens or self.max_tokens # Allow overriding max_tokens + max_temp, min_temp = max(temperature_range), min(temperature_range) # Ensure max > min + temperatures = [ + max_temp - i * (max_temp - min_temp) / max(1, num_responses - 1) + for i in range(num_responses) + ] + + if verbose: + print(f"Temperatures for responses: {temperatures}") + + candidates = [ + self.call_llm( + system_prompt=system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=max_tokens, + num_responses=1, + temperature=temp + )[0] # Extract the single response + for temp in temperatures + ] + + if self.log is not None: + self.log.append({"system_prompt": system_prompt, "user_prompt": user_prompt, "response": candidates}) + self.summary_log.append({'problem_instance': self.problem_instance(summary), 'summary': summary}) + + return candidates + + def select_candidate(self, candidates: List[Dict]) -> Dict: # Fixed type annotation + """ + Select the best response based on the responses. + Args: + candidates (List[Dict]): List of candidate responses as dictionaries. + Returns: + Dict: The selected candidate or an empty dictionary if no candidates exist. + """ + return candidates[-1] if candidates else {} # Default to the last candidate + + def _step( + self, verbose=False, mask=None, num_responses: int = 5, temperature_range: List[float] = [1.3, 0.], + selector: callable = None, *args, **kwargs + ) -> Dict: # Added type annotation for return value + """ + Perform a single optimization step, storing responses in self.responses and allowing selection. + Args: + verbose (bool): Whether to print debug information. + mask (list, optional): Mask for the problem instance. + num_responses (int): Number of responses to request from the LLM. + temperature (float): Sampling temperature for the LLM. + selector (callable, optional): Function to select the best response. + Returns: + Dict: The update dictionary based on the selected response. + """ + assert isinstance(self.propagator, GraphPropagator) + summary = self.summarize() + system_prompt, user_prompt = self.construct_prompt(summary, mask=mask) + + system_prompt = self.replace_symbols(system_prompt, self.prompt_symbols) + user_prompt = self.replace_symbols(user_prompt, self.prompt_symbols) + + # Generate candidates + responses = self.generate_candidates( + summary, system_prompt, user_prompt, verbose=verbose, mask=mask, + num_responses=num_responses, temperature_range=temperature_range + ) + + self.candidates = [] # Clear previous responses + for response in responses: + if "TERMINATE" in response: + self.candidates.append({}) + continue + + suggestion = self.extract_llm_suggestion(response) + update_dict = self.construct_update_dict(suggestion) + + self.candidates.append(update_dict) + + # Select the response using the selector or the default select_candidate method + if selector and callable(selector): # Ensure the selector is callable + self.selected_candidate = selector(self.candidates) + else: + self.selected_candidate = self.select_candidate(candidates=self.candidates) + + return self.selected_candidate From 6e8ff9501e79e5e7d1ac340555b669fb4117824d Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 23 Dec 2024 18:15:47 -0800 Subject: [PATCH 2/4] expose opto-prime multi's config API to the class construction. Add code to evaluate the implementation. --- .../evals/textgrad_solution_optimization.py | 12 ++++++++--- opto/optimizers/optoprimemulti.py | 20 ++++++++++++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/examples/textgrad_examples/evals/textgrad_solution_optimization.py b/examples/textgrad_examples/evals/textgrad_solution_optimization.py index 92c3a9e..abd2cfc 100644 --- a/examples/textgrad_examples/evals/textgrad_solution_optimization.py +++ b/examples/textgrad_examples/evals/textgrad_solution_optimization.py @@ -1,7 +1,7 @@ # This script applies Trace to optimize the workflow in TextGrad's solution_optimization.py. from opto import trace -from opto.optimizers import OptoPrime, TextGrad +from opto.optimizers import OptoPrime, TextGrad, OptoPrimeMulti import re import json @@ -162,6 +162,12 @@ def run_trace_test_time_training(sample): if args.algo == "textgrad": # This runs Trace's TextGrad optimizer optimizer = TextGrad([instance_var], max_tokens=16383) + elif args.algo == 'opto_multi': + # This runs Trace's OptoPrimeMulti optimizer + optimizer = OptoPrimeMulti([instance_var], + prompt_symbols={'variables': '#Parameters'}, + num_responses=3, + max_tokens=16383) else: # This runs Trace's OptoPrime optimizer optimizer = OptoPrime([instance_var], prompt_symbols={'variables': '#Parameters'}, @@ -215,7 +221,7 @@ def backfill(regret, maxlen): args = config() -assert args.algo in ["textgrad", "trace", "ttextgrad"], "ttextgrad is Trace's implementation textgrad" +assert args.algo in ["textgrad", "trace", "ttextgrad", 'opto_multi'], "ttextgrad is original implementation textgrad" llm_engine = tg.get_engine(engine_name=args.engine) tg.set_backward_engine(llm_engine, override=True) @@ -228,7 +234,7 @@ def backfill(regret, maxlen): with concurrent.futures.ThreadPoolExecutor(max_workers=args.num_threads) as executor: futures = [] for i, sample in enumerate(test_set): - if args.algo in ["trace", 'textgrad']: + if args.algo in ["trace", 'textgrad', 'opto_multi']: future = executor.submit(run_trace_test_time_training, sample) else: future = executor.submit(run_test_time_training, sample) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index 5f0c9c7..3f8824f 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -6,10 +6,17 @@ from opto.optimizers.optoprime import OptoPrime class OptoPrimeMulti(OptoPrime): - def __init__(self, *args, **kwargs): + def __init__(self, *args, + num_responses: int = 5, + temperature_range: List[float] = [1.3, 0.], + selector: callable = None, + **kwargs): super().__init__(*args, **kwargs) self.candidates = [] # Store all candidate solutions self.selected_candidate = None # Store the selected candidate solution + self.num_responses = num_responses + self.temperature_range = temperature_range + self.selector = selector def call_llm( self, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, @@ -41,6 +48,10 @@ def call_llm( print("LLM responses:\n", responses) return responses + def _default_value_rewrite(self, method_arg_value, class_set_value): + """Return method_arg_value if it is not equal to class_set_value, otherwise return class_set_value.""" + return method_arg_value if method_arg_value != class_set_value else class_set_value + def generate_candidates( self, summary, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, mask=None, max_tokens: int = None, num_responses: int = 5, temperature_range: List[float] = [1.3, 0.] @@ -59,6 +70,9 @@ def generate_candidates( Returns: List[str]: List of LLM responses as strings. """ + num_responses = self._default_value_rewrite(num_responses, self.num_responses) # Allow overriding num_responses + temperature_range = self._default_value_rewrite(temperature_range, self.temperature_range) + max_tokens = max_tokens or self.max_tokens # Allow overriding max_tokens max_temp, min_temp = max(temperature_range), min(temperature_range) # Ensure max > min temperatures = [ @@ -112,6 +126,10 @@ def _step( Returns: Dict: The update dictionary based on the selected response. """ + num_responses = self._default_value_rewrite(num_responses, self.num_responses) # Allow overriding num_responses + temperature_range = self._default_value_rewrite(temperature_range, self.temperature_range) + selector = self._default_value_rewrite(selector, self.selector) + assert isinstance(self.propagator, GraphPropagator) summary = self.summarize() system_prompt, user_prompt = self.construct_prompt(summary, mask=mask) From 16fdfee92dbb8c0949c0ce52f51fd98987750ba5 Mon Sep 17 00:00:00 2001 From: windweller Date: Mon, 30 Dec 2024 22:31:48 -0800 Subject: [PATCH 3/4] updated the __init__ and function signature --- opto/optimizers/optoprimemulti.py | 38 +++++++++++++++---------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index 3f8824f..cd55314 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -1,26 +1,28 @@ -from typing import Any, List, Dict, Union, Tuple +from typing import Any, List, Dict, Union, Tuple, Optional import json from textwrap import dedent from opto.trace.propagators import GraphPropagator from opto.optimizers.optoprime import OptoPrime + class OptoPrimeMulti(OptoPrime): def __init__(self, *args, num_responses: int = 5, - temperature_range: List[float] = [1.3, 0.], - selector: callable = None, + temperature_range: Optional[List[float]] = None, + selector: Optional[callable] = None, **kwargs): super().__init__(*args, **kwargs) + if temperature_range is None: + self.temperature_range = [1.3, 0.] self.candidates = [] # Store all candidate solutions self.selected_candidate = None # Store the selected candidate solution self.num_responses = num_responses - self.temperature_range = temperature_range self.selector = selector def call_llm( - self, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, - max_tokens: int = 4096, num_responses: int = 1, temperature: float = 0. + self, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, + max_tokens: int = 4096, num_responses: int = 1, temperature: float = 0. ) -> List[str]: """Call the LLM with a prompt and return multiple responses.""" if verbose not in (False, "output"): @@ -48,13 +50,9 @@ def call_llm( print("LLM responses:\n", responses) return responses - def _default_value_rewrite(self, method_arg_value, class_set_value): - """Return method_arg_value if it is not equal to class_set_value, otherwise return class_set_value.""" - return method_arg_value if method_arg_value != class_set_value else class_set_value - def generate_candidates( - self, summary, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, - mask=None, max_tokens: int = None, num_responses: int = 5, temperature_range: List[float] = [1.3, 0.] + self, summary, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, + mask=None, max_tokens: int = None, num_responses: Optional[int] = None, temperature_range: Optional[List[float]] = None ) -> List[str]: """ Generate multiple candidates with progressively decreasing temperatures. @@ -70,8 +68,8 @@ def generate_candidates( Returns: List[str]: List of LLM responses as strings. """ - num_responses = self._default_value_rewrite(num_responses, self.num_responses) # Allow overriding num_responses - temperature_range = self._default_value_rewrite(temperature_range, self.temperature_range) + num_responses = num_responses if num_responses is not None else self.num_responses # Allow overriding num_responses + temperature_range = temperature_range if temperature_range is not None else self.temperature_range max_tokens = max_tokens or self.max_tokens # Allow overriding max_tokens max_temp, min_temp = max(temperature_range), min(temperature_range) # Ensure max > min @@ -112,8 +110,8 @@ def select_candidate(self, candidates: List[Dict]) -> Dict: # Fixed type annota return candidates[-1] if candidates else {} # Default to the last candidate def _step( - self, verbose=False, mask=None, num_responses: int = 5, temperature_range: List[float] = [1.3, 0.], - selector: callable = None, *args, **kwargs + self, verbose=False, mask=None, num_responses: Optional[int] = None, temperature_range: Optional[List[float]] = None, + selector: callable = None, *args, **kwargs ) -> Dict: # Added type annotation for return value """ Perform a single optimization step, storing responses in self.responses and allowing selection. @@ -126,9 +124,9 @@ def _step( Returns: Dict: The update dictionary based on the selected response. """ - num_responses = self._default_value_rewrite(num_responses, self.num_responses) # Allow overriding num_responses - temperature_range = self._default_value_rewrite(temperature_range, self.temperature_range) - selector = self._default_value_rewrite(selector, self.selector) + num_responses = num_responses if num_responses is not None else self.num_responses # Allow overriding num_responses + temperature_range = temperature_range if temperature_range is not None else self.temperature_range + selector = selector if selector is not None else self.selector assert isinstance(self.propagator, GraphPropagator) summary = self.summarize() @@ -139,7 +137,7 @@ def _step( # Generate candidates responses = self.generate_candidates( - summary, system_prompt, user_prompt, verbose=verbose, mask=mask, + summary, system_prompt, user_prompt, verbose=verbose, mask=mask, num_responses=num_responses, temperature_range=temperature_range ) From 2423545555807ec92ef26cb8b4cf4831533e5f96 Mon Sep 17 00:00:00 2001 From: Xavier Daull Date: Thu, 16 Jan 2025 06:46:25 +0100 Subject: [PATCH 4/4] add improved candidate generation techniques and selector can take best of bread if not self refine is used for text generation --- opto/optimizers/optoprimemulti.py | 142 +++++++++++++++++++++++++----- 1 file changed, 118 insertions(+), 24 deletions(-) diff --git a/opto/optimizers/optoprimemulti.py b/opto/optimizers/optoprimemulti.py index cd55314..86461d0 100644 --- a/opto/optimizers/optoprimemulti.py +++ b/opto/optimizers/optoprimemulti.py @@ -19,6 +19,7 @@ def __init__(self, *args, self.selected_candidate = None # Store the selected candidate solution self.num_responses = num_responses self.selector = selector + self.use_synthesis = False def call_llm( self, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, @@ -52,10 +53,10 @@ def call_llm( def generate_candidates( self, summary, system_prompt: str, user_prompt: str, verbose: Union[bool, str] = False, - mask=None, max_tokens: int = None, num_responses: Optional[int] = None, temperature_range: Optional[List[float]] = None + mask=None, max_tokens: int = None, num_responses: Optional[int] = None, temperature_range: Optional[List[float]] = None, generation_technique: str = "temperature_variation" ) -> List[str]: """ - Generate multiple candidates with progressively decreasing temperatures. + Generate multiple candidates using configurable techniques. Args: summary: The summarized problem instance. system_prompt (str): The system-level prompt. @@ -65,49 +66,142 @@ def generate_candidates( max_tokens (int, optional): Maximum token limit for the LLM responses. num_responses (int): Number of responses to request. temperature_range (List[float]): [max_temperature, min_temperature]. + generation_technique (str): Technique for generating candidates. Options: + - "temperature_variation": Use temperature range for diversity (default). + - "self_refinement": Iteratively refine candidates using self-feedback. + - "iterative_alternatives": Find new alternative optimal solutions given previous candidates. Returns: List[str]: List of LLM responses as strings. """ num_responses = num_responses if num_responses is not None else self.num_responses # Allow overriding num_responses temperature_range = temperature_range if temperature_range is not None else self.temperature_range - max_tokens = max_tokens or self.max_tokens # Allow overriding max_tokens - max_temp, min_temp = max(temperature_range), min(temperature_range) # Ensure max > min - temperatures = [ - max_temp - i * (max_temp - min_temp) / max(1, num_responses - 1) - for i in range(num_responses) - ] - if verbose: - print(f"Temperatures for responses: {temperatures}") + candidates = [] - candidates = [ - self.call_llm( - system_prompt=system_prompt, - user_prompt=user_prompt, - verbose=verbose, - max_tokens=max_tokens, - num_responses=1, - temperature=temp - )[0] # Extract the single response - for temp in temperatures - ] + # Temperature Variation (Original Logic) + if generation_technique == "temperature_variation": + self.use_synthesis = True # Enable synthesis for the final selection + max_temp, min_temp = max(temperature_range), min(temperature_range) + temperatures = [ + max_temp - i * (max_temp - min_temp) / max(1, num_responses - 1) + for i in range(num_responses) + ] + + if verbose: + print(f"Temperatures for responses: {temperatures}") + + candidates = [ + self.call_llm( + system_prompt=system_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=max_tokens, + num_responses=1, + temperature=temp + )[0] # Extract the single response + for temp in temperatures + ] + + # Self-Refinement + elif generation_technique == "self_refinement": + for _ in range(num_responses): + if not candidates: # First candidate, no refinement needed + current_prompt = system_prompt + else: # Refine the last candidate + current_prompt = f"{system_prompt}\nRefine the following solution: {candidates[-1]}" + + candidate = self.call_llm( + system_prompt=current_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=max_tokens, + num_responses=1, + temperature=0. # Deterministic output + )[0] + candidates.append(candidate) + + # Iterative Alternatives + elif generation_technique == "iterative_alternatives": + self.use_synthesis = True # Enable synthesis for the final selection + for i in range(num_responses): + if not candidates: # First candidate, no alternatives yet + current_prompt = system_prompt + else: # Generate a new alternative based on previous candidates + previous_solutions = "\n".join( + f"SOLUTION {idx + 1}: <<<{candidate}>>>" + for idx, candidate in enumerate(candidates) + ) + current_prompt = ( + f"{system_prompt}\nGiven the following solutions, propose a new alternative optimal solution:\n" + f"{previous_solutions}\n{user_prompt}" + ) + + candidate = self.call_llm( + system_prompt=current_prompt, + user_prompt=user_prompt, + verbose=verbose, + max_tokens=max_tokens, + num_responses=1, + temperature=0. # Deterministic output + )[0] + candidates.append(candidate) + + else: + raise ValueError(f"Invalid generation_technique: {generation_technique}. " + "Supported options: 'temperature_variation', 'self_refinement', " + "'iterative_alternatives'.") + # Log the generated candidates if self.log is not None: self.log.append({"system_prompt": system_prompt, "user_prompt": user_prompt, "response": candidates}) self.summary_log.append({'problem_instance': self.problem_instance(summary), 'summary': summary}) return candidates - def select_candidate(self, candidates: List[Dict]) -> Dict: # Fixed type annotation + def select_candidate(self, candidates: List[Dict], use_synthesis: bool = False) -> Dict: """ Select the best response based on the responses. Args: candidates (List[Dict]): List of candidate responses as dictionaries. + use_synthesis (bool): If True, synthesize an optimal solution from all candidates. Returns: Dict: The selected candidate or an empty dictionary if no candidates exist. """ - return candidates[-1] if candidates else {} # Default to the last candidate + if not candidates: + return {} + + # Default behavior: return the last candidate + if not use_synthesis: + return candidates[-1] + + # Synthesize an optimal solution from all candidates + candidate_texts = [f"SOLUTION {i + 1}: <<<{json.dumps(candidate, indent=2)}>>>" for i, candidate in enumerate(candidates)] + synthesis_prompt = ( + "Given the following solutions and the initial question, provide an optimal solution by combining the best elements of each. Follow the same output structure as the candidates.\n\n" + "Candidates:\n" + "\n".join(candidate_texts) + "\n\n" + "Optimal Solution:\n" + ) + + # Call the LLM to synthesize the optimal solution + synthesized_response = self.call_llm( + system_prompt="You are an expert optimizer. Synthesize the best solution from the given candidates.", + user_prompt=synthesis_prompt, + verbose=False, + #max_tokens=??, + num_responses=1, + temperature=0.3 # Low temperature for deterministic synthesis + ) + + if synthesized_response: + try: + return json.loads(synthesized_response[0]) + except json.JSONDecodeError: + # Fallback to the last candidate if synthesis fails + return candidates[-1] + else: + # Fallback to the last candidate if synthesis fails + return candidates[-1] def _step( self, verbose=False, mask=None, num_responses: Optional[int] = None, temperature_range: Optional[List[float]] = None, @@ -156,6 +250,6 @@ def _step( if selector and callable(selector): # Ensure the selector is callable self.selected_candidate = selector(self.candidates) else: - self.selected_candidate = self.select_candidate(candidates=self.candidates) + self.selected_candidate = self.select_candidate(candidates=self.candidates, use_synthesis=self.use_synthesis) return self.selected_candidate