From 18235e3a6d63fd846f9647e1e767fa7ef980a986 Mon Sep 17 00:00:00 2001 From: Robert Clark Date: Mon, 20 Oct 2025 09:32:18 -0700 Subject: [PATCH] Add SDG starter kit Create a Starter Kit which deploys an LLM as an Endpoint for synthetic data generation and sends sample requests to the deployed model. Signed-Off-By: Robert Clark --- .../images/chat-data-curation-pipeline.png | Bin 0 -> 37491 bytes .../synthetic-qa-data-gen-with-nemotron.ipynb | 775 ++++++++++++++++++ 2 files changed, 775 insertions(+) create mode 100644 lepton/starterkits/images/chat-data-curation-pipeline.png create mode 100644 lepton/starterkits/synthetic-qa-data-gen-with-nemotron.ipynb diff --git a/lepton/starterkits/images/chat-data-curation-pipeline.png b/lepton/starterkits/images/chat-data-curation-pipeline.png new file mode 100644 index 0000000000000000000000000000000000000000..759f732829c89337b846e0e0f48ad49b75b3b4ea GIT binary patch literal 37491 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 cV&6?5q_lJY=($wtFMidyC%G1yBLDyZ literal 0 HcmV?d00001 diff --git a/lepton/starterkits/synthetic-qa-data-gen-with-nemotron.ipynb b/lepton/starterkits/synthetic-qa-data-gen-with-nemotron.ipynb new file mode 100644 index 0000000..f7a2836 --- /dev/null +++ b/lepton/starterkits/synthetic-qa-data-gen-with-nemotron.ipynb @@ -0,0 +1,775 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "cde135f2", + "metadata": {}, + "source": [ + "# Data Curation with NIMs: Synthetic Chat QA Generation\n", + "\n", + "For chat data, prompts can come from both public, real-world user interactions and a synthetic data generation pipeline. Synthetic prompts cover various tasks such as open QA, closed QA, and creative writing. \n", + "\n", + "For each prompt task, the LLM can be seeded for generation with a diverse set of topics or keywords so that the prompts cover a wide variety of topics. For the responses, LLMs can be prompted for multiple generations in order to do rejection sampling with a reward model and generate higher quality synthetic data.\n" + ] + }, + { + "cell_type": "markdown", + "id": "7db5817a", + "metadata": {}, + "source": [ + "### Requirements:\n", + "* Container: `nvcr.io/nvidia/nemo:25.07`\n", + "* GPUs: One NVIDIA GPU with at least 20 GB of GPU memory is required for Endpoint deployment.\n", + "* Storage: To persist datasets, mount shared storage in the Dev Pod.\n", + "\n", + "---\n", + "\n", + "**Table of Contents**\n", + "\n", + "* API Key Setup\n", + "* OpenAI Client\n", + "* Prompt Templates\n", + " * Topics & Subtopics\n", + " * Generating Questions\n", + " * Generating Responses to Questions\n", + "* Summary & Next Steps\n", + "\n", + "---" + ] + }, + { + "cell_type": "markdown", + "id": "96ed21a2", + "metadata": {}, + "source": [ + "![Figure 1. Chat data curation pipeline](images/chat-data-curation-pipeline.png)\n" + ] + }, + { + "cell_type": "markdown", + "id": "543142a2", + "metadata": {}, + "source": [ + "The following notebook will demonstrate how to leverage [Nemotron-nano-9b-v2](https://build.nvidia.com/nvidia/nvidia-nemotron-nano-9b-v2) through build.nvidia.com to synthetically generate chat QA data.\n", + "The pipeline is designed to create a preference dataset suitable for training a reward model using the [SteerLM method](https://docs.nvidia.com/nemo-framework/user-guide/latest/modelalignment/steerlm.html), which can be used for training an RLHF Reward Model or for DPO with a framework, such as [NeMo-RL](https://github.com/NVIDIA-NeMo/RL)." + ] + }, + { + "cell_type": "markdown", + "id": "73c59386", + "metadata": {}, + "source": [ + "### Inference Endpoint Setup\n", + "In this notebook, we will launch an inference server using DGX Cloud Lepton's Endpoints to be able to send prompts to a model. The notebook walks through this process, but you can also use `build.nvidia.com` to send an API request to an existing NVIDIA-managed endpoint - more on that later on.\n", + "\n", + "First, we need to setup an Endpoint to host the model we will use for generating datasets. The cell below configures several environment settings required for accessing DGX Cloud Lepton. You will need an API key for accessing your workspace. Follow [this link](https://docs.nvidia.com/dgx-cloud/lepton/features/workspace/token/) to generate an API key for authenticating with your cluster. Once generated, add it to the `LEPTON_KEY` output in the format `:` (without brackets) where `` is the unique identifier for your workspace.\n", + "\n", + "Next, update the `RESOURCE_SHAPE` and `NODE_GROUP` variables to match your workspace. The default model only needs approximately 20 GB of GPU memory so select an appropriate GPU resource for deploying the model." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37b79cc5-0297-4668-b2a0-e9f2aef11af0", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import re\n", + "import subprocess\n", + "import time\n", + "\n", + "BASE_MODEL = \"nvidia/nvidia-nemotron-nano-9b-v2\" # Optionally use a different model for requests\n", + "ACCESS_TOKEN = \"my-access-token\" # Set the password for authenticating endpoint requests\n", + "SAVE_DIRECTORY = \"\" # Specify the absolute path to save the generated data. To save on shared storage, must be a mounted storage path\n", + "\n", + "os.environ[\"LEPTON_KEY\"] = \":\" # Set the workspace ID and Lepton API key for authenticating with DGX Cloud Lepton\n", + "os.environ[\"RESOURCE_SHAPE\"] = \"gpu.1xh200\" # Select the appropriate GPU resource for deploying the model\n", + "os.environ[\"NODE_GROUP\"] = \"\" # Select the appropriate node group for deploying the model\n", + "os.environ[\"ACCESS_TOKEN\"] = ACCESS_TOKEN # Set the password for authenticating endpoint requests\n", + "os.environ[\"BASE_MODEL\"] = BASE_MODEL # Set the model to be used for data generation\n", + "os.environ[\"ENDPOINT_NAME\"] = \"nemotron-nano-9b-v2\" # Set the name for the endpoint" + ] + }, + { + "cell_type": "markdown", + "id": "b873dd46", + "metadata": {}, + "source": [ + "Now we authenticate with DGX Cloud Lepton using the provided credentials and deploy the model as an Endpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bb4eb12-6da8-4ee0-97fd-f35949e37ef5", + "metadata": {}, + "outputs": [], + "source": [ + "%%bash\n", + "\n", + "lep login -c $LEPTON_KEY\n", + "\n", + "lep endpoint create \\\n", + "--resource-shape $RESOURCE_SHAPE \\\n", + "--node-group $NODE_GROUP \\\n", + "--container-image \"vllm/vllm-openai\" \\\n", + "--container-port 8080 \\\n", + "--container-command \"vllm serve ${BASE_MODEL} --port 8080 --trust_remote_code\" \\\n", + "--name $ENDPOINT_NAME \\\n", + "--tokens $ACCESS_TOKEN" + ] + }, + { + "cell_type": "markdown", + "id": "f26998c5", + "metadata": {}, + "source": [ + "Wait for the Endpoint to be available for requests:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f949c89-479a-48b7-831b-84b427174beb", + "metadata": {}, + "outputs": [], + "source": [ + "def wait_for_endpoint(endpoint_name: str, interval: int = 10) -> str:\n", + " command = [\"lep\", \"endpoint\", \"status\", \"-n\", endpoint_name, \"--detail\"]\n", + " while True:\n", + " result = subprocess.run(command, capture_output=True, text=True, check=True)\n", + " for line in result.stdout.split(\"\\n\"):\n", + " if line.startswith(\"State\"):\n", + " _, state = line.strip().rsplit(\" \", maxsplit=1)\n", + " if \"LeptonDeploymentState.Ready\" in state:\n", + " print(\"Endpoint deployed!\")\n", + " else:\n", + " break\n", + " url_match = re.search(r'https://[\\w\\d\\.\\-]+', line)\n", + " if url_match:\n", + " print(f\"URL: {url_match[0]}\")\n", + " return url_match[0]\n", + " print(f\"Waiting for endpoint {endpoint_name} to be ready...\")\n", + " time.sleep(interval)\n", + "\n", + "endpoint_url = wait_for_endpoint(os.environ[\"ENDPOINT_NAME\"])\n", + "base_url = os.path.join(endpoint_url, \"v1\")" + ] + }, + { + "cell_type": "markdown", + "id": "b20bc968", + "metadata": {}, + "source": [ + "### OpenAI Client\n", + "\n", + "Now we're going to:\n", + "1. Initialize OpenAI's client.\n", + "2. Configure the LLM parameters.\n", + "3. Perform a request, and print the LLM response.\n", + "\n", + "
\n", + "NOTE: As stated above, this notebook relies on a DGX Cloud Lepton Endpoint, but you can instead deploy with an existing OpenAI-compatible API service, such as `build.nvidia.com`. If desired, generate a personal API key with:
\n", + "1. Login (or sign up) through build.nvidia.com.
\n", + "2. Click the View Code button and then Generate API Key available on the nvidia/nemotron-nano-9b-v2 model.

\n", + "\n", + "Next, update `base_url` to be `https://integrate.api.nvidia.com/v1` and `ACCESS_TOKEN` to your personal API key that was generated.\n", + "
\n", + "\n", + "" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c146a96", + "metadata": {}, + "outputs": [], + "source": [ + "from openai import OpenAI\n", + "\n", + "#ACCESS_TOKEN = \"my-nv-api-key\" # Optional: Use NVIDIA's API service instead of the deployed endpoint\n", + "\n", + "# Initialize the OpenAI client for NVIDIA's API\n", + "client = OpenAI(\n", + " #base_url = \"https://integrate.api.nvidia.com/v1\", # Optional: Use NVIDIA's API service instead of the deployed endpoint\n", + " base_url = base_url,\n", + " api_key = ACCESS_TOKEN\n", + ")\n", + "\n", + "# Example: Generate a limerick about GPU computing\n", + "completion = client.chat.completions.create(\n", + " model=BASE_MODEL,\n", + " messages=[{\"role\":\"user\",\"content\":\"Write a limerick about the wonders of GPU computing.\"}],\n", + " temperature=0.6,\n", + " top_p=0.95,\n", + " max_tokens=2048,\n", + " frequency_penalty=0,\n", + " presence_penalty=0,\n", + " stream=True,\n", + " extra_body={\n", + " \"min_thinking_tokens\": 1024,\n", + " \"max_thinking_tokens\": 2048\n", + " }\n", + ")\n", + "\n", + "# Print the streamed response\n", + "for chunk in completion:\n", + " reasoning = getattr(chunk.choices[0].delta, \"reasoning_content\", None)\n", + " if reasoning:\n", + " print(reasoning, end=\"\")\n", + " if chunk.choices[0].delta.content is not None:\n", + " print(chunk.choices[0].delta.content, end=\"\")" + ] + }, + { + "cell_type": "markdown", + "id": "fee40876", + "metadata": {}, + "source": [ + "### Prompt Templates\n", + "\n", + "To generate questions and responses, we need a prompt template so that the model can understand how to generate the data.\n", + "\n", + "We will use:\n", + "* A prompt template to generate subtopics from a user provided topic\n", + "* A prompt template to generate questions for a given subtopic\n", + "* A prompt template to generate responses for a given question" + ] + }, + { + "cell_type": "markdown", + "id": "82172dd4", + "metadata": {}, + "source": [ + "#### Topics and Subtopics" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32b0a989", + "metadata": {}, + "outputs": [], + "source": [ + "TOPIC_GENERATION_PROMPT_TEMPLATE = \"\"\"\\\n", + "Given a topic, generate a list of {n_subtopics} subtopics that are related to the topic.\n", + "\n", + "The topic is: {topic}\n", + "\n", + "The list must be without numbers, and without any description of the subtopics. The subtopics should be separated by a comma. There must be no other text than the list.\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "d6b039d3", + "metadata": {}, + "source": [ + "We can use `AsyncOpenAI` to allow nested event loops for async code execution." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "431b7983", + "metadata": {}, + "outputs": [], + "source": [ + "from openai import AsyncOpenAI\n", + "\n", + "client = AsyncOpenAI(\n", + " base_url = base_url,\n", + " api_key = ACCESS_TOKEN\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99a5b4cb", + "metadata": {}, + "outputs": [], + "source": [ + "async def generate_subtopics(client, topic, n_subtopics): \n", + " prompt = TOPIC_GENERATION_PROMPT_TEMPLATE.format(topic=topic, n_subtopics=n_subtopics)\n", + " return await client.chat.completions.create(\n", + " model=BASE_MODEL,\n", + " messages=[\n", + " {\"role\": \"user\", \"content\": prompt},\n", + " ],\n", + " temperature=0.2,\n", + " top_p=0.7,\n", + " max_tokens=1024,\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a56630b", + "metadata": {}, + "outputs": [], + "source": [ + "# Example topic and number of subtopics to generate\n", + "# (You can change these values as needed)\n", + "topic = \"Wales\"\n", + "n_subtopics = 5" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8b6bf777", + "metadata": {}, + "outputs": [], + "source": [ + "responses = await generate_subtopics(client, topic=topic, n_subtopics=n_subtopics)\n", + "nonreasoning_answer = re.sub(r'.*', \"\", responses.choices[0].message.content, flags=re.DOTALL).strip()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b99a8823", + "metadata": {}, + "outputs": [], + "source": [ + "print(nonreasoning_answer)" + ] + }, + { + "cell_type": "markdown", + "id": "9fb3545b", + "metadata": {}, + "source": [ + "Now we can store the generated subtopics as a list for the next stage." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e239af15", + "metadata": {}, + "outputs": [], + "source": [ + "subtopic_list = nonreasoning_answer.split(\",\")" + ] + }, + { + "cell_type": "markdown", + "id": "53116ebb", + "metadata": {}, + "source": [ + "#### Generating Questions\n", + "\n", + "With a list of subtopics, the next step is to generate a set of questions for each subtopic. The following code defines a function to batch-generate questions using the LLM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "541bb24d", + "metadata": {}, + "outputs": [], + "source": [ + "QUESTION_PROMPT_TEMPLATE = \"\"\"\\\n", + "Given a topic, generate {n_questions} questions that could be asked about that topic. Your response should be in a list format.\n", + "\n", + "The topic is: {sub_topic}\n", + "\n", + "The list must be without numbers. The questions should be separated by a newline character. There must be no other text than the list.\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "937bc8c8", + "metadata": {}, + "outputs": [], + "source": [ + "async def generate_questions(client, sub_topic, n_questions):\n", + " prompt = QUESTION_PROMPT_TEMPLATE.format(sub_topic=sub_topic, n_questions=n_questions)\n", + " response = await client.chat.completions.create(\n", + " model=BASE_MODEL,\n", + " messages=[\n", + " {\"role\": \"system\", \"content\": \"/no_think\"},\n", + " {\"role\": \"user\", \"content\": prompt},\n", + " ],\n", + " temperature=0.2,\n", + " top_p=0.7,\n", + " max_tokens=1024,\n", + " )\n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2fad8dc0", + "metadata": {}, + "outputs": [], + "source": [ + "import asyncio\n", + "\n", + "async def question_generator(client, subtopic_list, n_question): \n", + " tasks = [generate_questions(client, subtopic, n_question) for subtopic in subtopic_list]\n", + " return await asyncio.gather(*tasks)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4dc2fae0", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "n_questions = 5\n", + "question_list = await question_generator(client, subtopic_list, n_questions)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85c6a7a2", + "metadata": {}, + "outputs": [], + "source": [ + "question_list" + ] + }, + { + "cell_type": "markdown", + "id": "d471e352", + "metadata": {}, + "source": [ + "Now we can convert the questions into a single long list for downstream response generation." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8906c80e", + "metadata": {}, + "outputs": [], + "source": [ + "question_list_formatted = []\n", + "\n", + "for question_set in question_list:\n", + " question_list_formatted += question_set.split(\"\\n\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4341f631", + "metadata": {}, + "outputs": [], + "source": [ + "len(question_list_formatted)" + ] + }, + { + "cell_type": "markdown", + "id": "f573900e", + "metadata": {}, + "source": [ + "#### Generating Responses to Questions\n", + "\n", + "Using the question list, we can prompt `Nemotron-nano-9b-v2` to generate multiple responses for each question. \n", + "\n", + "Note: This section includes concurrency control to avoid rate limit errors using the API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d44f2d6", + "metadata": {}, + "outputs": [], + "source": [ + "RESPONSE_PROMPT_TEMPLATE = \"\"\"\\\n", + "Given a question, generate 2 responses that could be given to that question. Your response should be in a list format.\n", + "\n", + "The question is: {question}\n", + "\n", + "The list must be in the format:\n", + "\n", + "RESPONSE A: Response A text here\n", + "RESPONSE B: Response B text here\n", + "\"\"\"" + ] + }, + { + "cell_type": "markdown", + "id": "c2f072ce", + "metadata": {}, + "source": [ + "**Tip:** Limit concurrency with a semaphore to avoid API rate limit errors when generating many responses in parallel." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20f1b99d", + "metadata": {}, + "outputs": [], + "source": [ + "async def generate_response(client, question, sem=None):\n", + " prompt = RESPONSE_PROMPT_TEMPLATE.format(question=question)\n", + " async with sem or asyncio.Semaphore(1): \n", + " response = await client.chat.completions.create(\n", + " model=BASE_MODEL,\n", + " messages=[{\"role\": \"user\", \"content\": prompt}],\n", + " temperature=0.2,\n", + " top_p=0.7,\n", + " max_tokens=1024,\n", + " )\n", + " return response.choices[0].message.content" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24a325d1", + "metadata": {}, + "outputs": [], + "source": [ + "async def response_generator(client, question_list, max_concurrent=5):\n", + " sem = asyncio.Semaphore(max_concurrent) if max_concurrent else None\n", + " tasks = [generate_response(client, question, sem) for question in question_list]\n", + " return await asyncio.gather(*tasks)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3fc3e91c", + "metadata": {}, + "outputs": [], + "source": [ + "question_response_list = await response_generator(client, question_list_formatted, max_concurrent=5)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76edbda2", + "metadata": {}, + "outputs": [], + "source": [ + "question_response_pair_list = []\n", + "\n", + "for question, response_set in zip(question_list_formatted, question_response_list, strict=False):\n", + " question_response_pair_list.append(\n", + " {\n", + " \"question\": question,\n", + " \"responses\": {\n", + " \"response_a\": {\"response\": response_set.split(\"RESPONSE B:\")[0].replace(\"RESPONSE A:\", \"\").strip()},\n", + " \"response_b\": {\"response\": response_set.split(\"RESPONSE B:\")[-1].split(\"\\n\\n\")[0].strip()},\n", + " },\n", + " },\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e834b46", + "metadata": {}, + "outputs": [], + "source": [ + "# Save the generated question-response pairs to a JSONL file for downstream use.\n", + "import json\n", + "\n", + "save_path = os.path.join(SAVE_DIRECTORY, \"synthetic_data.jsonl\")\n", + "\n", + "with open(save_path, \"w\") as f:\n", + " for item in question_response_pair_list:\n", + " f.write(json.dumps(item))\n", + " f.write(\"\\n\")\n", + "\n", + "print(f\"Saved {len(question_response_pair_list)} question-response pairs to {save_path}\")" + ] + }, + { + "cell_type": "markdown", + "id": "03c49613", + "metadata": {}, + "source": [ + "### Math Problems\n", + "\n", + "We can also generate math problems based on a specific subtopic, using a custom parser." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5df37786", + "metadata": {}, + "outputs": [], + "source": [ + "MATH_PROMPT_TEMPLATE = (\n", + " \"Create {n_problems} diverse mathematics problems related to the topic '{topic}' \"\n", + " \"or solvable using concepts from '{topic}'. Provide your response as a numbered list, \"\n", + " \"and include both the problem description and its solution. Format your response as follows: \"\n", + " \"((###1###)) >>>Problem<<<: [Description of the first problem]. >>>Solution<<<: [Solution to the first problem].\\n\"\n", + " \"((###2###)) >>>Problem<<<: [Description of the second problem]. >>>Solution<<<: [Solution to the second problem].\\n\"\n", + " \"Only include the problems and their solutions—no additional text.\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e8f4c408", + "metadata": {}, + "outputs": [], + "source": [ + "n_problems = 5\n", + "topic = \"Algebra\"" + ] + }, + { + "cell_type": "markdown", + "id": "06fa0b23", + "metadata": {}, + "source": [ + " In this case, we will use a customer parser to clean our list of questions/answers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cc8a83dc", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import List, Tuple\n", + "\n", + "def parse_math_llm_response(\n", + " llm_response: str, tag_replacements: dict\n", + " ) -> List[Tuple[str, str]]:\n", + " \"\"\"\n", + " Expects response from LLM to be wrapped between 2 distinct tags (>>>Problem<<<: and >>>Solution<<<:).\n", + " Uses regex to extract each problem-solution pair, then cleans and replaces original tags with\n", + " more user-friendly labels.\n", + " \"\"\"\n", + " \n", + " first_tag, second_tag = list(tag_replacements.keys())[:2]\n", + "\n", + " pattern = rf\"{re.escape(first_tag)}(.*?){re.escape(second_tag)}(.*?)(?=\\(\\(###|\\Z)\"\n", + " matches = re.findall(pattern, llm_response, re.DOTALL)\n", + "\n", + " problem_solution_pairs = []\n", + " for problem, solution in matches:\n", + " for tag, replacement in tag_replacements.items():\n", + " problem = problem.replace(tag, replacement).strip()\n", + " solution = solution.replace(tag, replacement).strip()\n", + " problem_solution_pairs.append((problem.strip(), solution.strip()))\n", + "\n", + " return problem_solution_pairs\n", + "\n", + "\n", + "async def generate_math(\n", + " client,\n", + " topic: str,\n", + " n_problems: int,\n", + " math_prompt_template: str,\n", + " tag_replacements: dict,\n", + " n_retries: int = 3,\n", + ") -> List[Tuple[str, str]]:\n", + " \"\"\"\n", + " Async function to generate math problems and parse them into (problem, solution) pairs.\n", + " \"\"\"\n", + " math_problems = []\n", + " prompt = math_prompt_template.format(topic=topic, n_problems=n_problems)\n", + "\n", + " for attempt in range(n_retries):\n", + " try:\n", + " response = await client.chat.completions.create(\n", + " model=BASE_MODEL,\n", + " messages=[{\"role\": \"user\", \"content\": prompt}],\n", + " temperature=0.1,\n", + " top_p=0.9,\n", + " max_tokens=1024,\n", + " )\n", + "\n", + " # Extract raw LLM text\n", + " llm_response = response.choices[0].message.content\n", + "\n", + " # Parse with your existing parser\n", + " math_problems = parse_math_llm_response(llm_response, tag_replacements)\n", + "\n", + " break # success, exit retries\n", + "\n", + " except Exception as e:\n", + " print(f\"Attempt {attempt+1}/{n_retries} failed: {e}\")\n", + "\n", + " return math_problems\n", + "\n", + "\n", + "# Example usage\n", + "tag_replacements = {\n", + " \">>>Problem<<<:\": \"Problem:\",\n", + " \">>>Solution<<<:\": \"Solution:\",\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42a7d224", + "metadata": {}, + "outputs": [], + "source": [ + "math_problems = await generate_math(client, topic, n_problems, MATH_PROMPT_TEMPLATE, tag_replacements)\n", + "\n", + "for idx, (problem, solution) in enumerate(math_problems):\n", + " print(f\"Problem {idx+1}: {problem}\")\n", + " print(f\"Solution {idx+1}: {solution}\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "6367ef60", + "metadata": {}, + "source": [ + "### Summary & Next Steps\n", + "\n", + "- You have now generated a synthetic chat QA dataset using NVIDIA's Nemotron LLMs.\n", + "- The data is saved in `synthetic_data.jsonl` and ready for use in reward modeling or DPO training.\n", + "- Next steps: Evaluate the data, filter for quality, and consider augmenting with real-world data for best results.\n", + "\n", + "---" + ] + } + ], + "metadata": { + "container_image": "nvcr.io/nvidia/nemo:25.07", + "description": "Tutorial on generating synthetic datasets using NIMs for inference", + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.3" + }, + "title": "Data Curation with NIMs: Synthetic Chat QA Generation" + }, + "nbformat": 4, + "nbformat_minor": 5 +}