Add Math pipeline - #1058
Conversation
|
Thank you for your work and submission. I'd like to ask if this is the code for the paper "NEMOTRON-CC-MATH: A 133 BILLION-TOKENSCALE HIGH QUALITY MATH PRETRAINING DATASET"? How do you plan to merge this into the updated master branch? |
Dear ronjer30, I attempted to run your PR using the latest nemo-curator:25.07 image on only one node. However, I keep encountering this error: "Task was killed due to the node running low on memory. Memory on the node (IP: *****, ID: *****) where the task (task ID: xxxxxx name=StageWorker.init, pid=263827, memory used=0.08GB) was running was 205.81GB / 216.21GB (0.951878), which exceeds the memory usage threshold of 0.95. " To address this issue, I reduced the data size to no more than 100 words, yet the error still occurs. Do you have any suggestions on how to resolve this? Thank you! |
Signed-off-by: RanjitR <ranjitr@nvidia.com>
Signed-off-by: RanjitR <ranjitr@nvidia.com>
- Enhanced LynxExtractor with error handling - Improved FineMathClassifier with center cropping and optimized defaults - Added comprehensive test coverage for all changes Signed-off-by: RanjitR <ranjitr@nvidia.com>
0d8b68b to
6da8d3d
Compare
| @staticmethod | ||
| def _mid_slice(s: str, n: int) -> str: | ||
| m = len(s) // 2 | ||
| b, e = max(0, m - n), min(m + n, len(s) - 1) |
There was a problem hiding this comment.
logic: off-by-one error truncates last character when text doesn't need cropping
| b, e = max(0, m - n), min(m + n, len(s) - 1) | |
| b, e = max(0, m - n), min(m + n, len(s)) |
Please try with |
Greptile SummaryThis PR introduces a complete math data pipeline for NeMo Curator, covering HuggingFace dataset download ( Key changes:
Issues found:
Confidence Score: 3/5
Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[0_download.py\nHuggingFace Parquet download] --> B[1_cc_index_lookup.py\nCommon Crawl index lookup]
B --> C[2_text_preprocess.py\nCommonCrawlWARCReader + MathExtractStage]
C --> D[3_llm_cleanup.py\nTokenSplitterStage → LLMCleanupStage → ChunkMergeStage]
D --> E[4_quality_classifier.py\nFineMathClassifier]
E --> F[5_deduplication.py\nFuzzyDeduplication]
subgraph MathExtractStage
C1[MathContentExtractor]
C1 --> C2{magic MIME detect}
C2 -->|HTML| C3[LynxExtractor]
C2 -->|Notebook| C4[_notebook_to_text]
C2 -->|Text| C5[raw text]
end
subgraph LLM Cleanup Pipeline
D1[TokenSplitterStage\nparagraph-aware chunking] --> D2[LLMCleanupStage\nvLLM inference]
D2 --> D3[ChunkMergeStage\ngroupby + concat]
end
subgraph FineMathClassifier
E1[CenterCropTextStage] --> E2[TokenizerStage]
E2 --> E3[FineMathModelStage\nDeBERTa sequence classifier]
end
Last reviewed commit: 3171358 |
|
|
||
|
|
||
| def build_pipeline(input_glob: str, output_dir: str) -> Pipeline: | ||
| p = Pipeline(name="math_quality_classifier", description="...") |
There was a problem hiding this comment.
style: placeholder description should be replaced with meaningful text describing the pipeline's purpose
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| if row["chunk_id"] < len(result.data) - 1: | ||
| # Non-last chunks should end with separator | ||
| assert row["text"].endswith("\n") or len(result.data) == 1 |
There was a problem hiding this comment.
logic: condition logic is incorrect - when there's only one chunk, it should not end with separator regardless of chunk_id
| --input DATA_DIR \ | ||
| --output OUTPUT_DIR \ | ||
| --model microsoft/phi-4 \ | ||
| --prompt HTML_TO_TEXT_PROMPT \ |
There was a problem hiding this comment.
style: HTML_TO_TEXT_PROMPT appears to be undefined - should this be a literal string or reference to a constant? Should HTML_TO_TEXT_PROMPT be replaced with an actual prompt string or is this a placeholder that users should replace?
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
This is intentional. HTML_TO_TEXT_PROMPT is the name of a constant that is used to fetch the prompt from prompts.py
| --input_filetype parquet | ||
| ``` | ||
|
|
||
| This will chunk the data and clean each chunk, creating output in `OUTPUT_CHUNK_DIR/cleanup_*/` with: |
There was a problem hiding this comment.
syntax: OUTPUT_CHUNK_DIR is undefined but should likely be OUTPUT_DIR based on the command above
| This will chunk the data and clean each chunk, creating output in `OUTPUT_CHUNK_DIR/cleanup_*/` with: | |
| This will chunk the data and clean each chunk, creating output in `OUTPUT_DIR/cleanup_*/` with: |
| user_prompt = self.system_prompt.format(text=text) | ||
|
|
||
| if is_qwen3_30b_a3b: | ||
| user_prompt = user_prompt + " /nothink" |
There was a problem hiding this comment.
logic: typo in special token - should likely be "/nothinking" not "/nothink". Is "/nothink" the correct special token for Qwen3-30B-A3B, or should this be "/nothinking"?
There was a problem hiding this comment.
From the Huggingface entry, the correct token for this is /no_think. Will make the change
| """ | ||
|
|
||
| mind_two_students = """Convert the context below as a multi-turn discussions between two students who are working on their assignment related to the given context. \ | ||
| Make sure that their discussions strictly adhere to the context below and remains faithful to infomration in the context. \ |
There was a problem hiding this comment.
syntax: typo: 'infomration' should be 'information'
| Make sure that their discussions strictly adhere to the context below and remains faithful to infomration in the context. \ | |
| Make sure that their discussions strictly adhere to the context below and remains faithful to information in the context. \ |
| """ | ||
|
|
||
| mind_interview = """Conduct an interview-style conversation where one participant acts as the interviewer, asking questions exclusively related to the content provided, while the other participant serves as the subject matter expert, providing detailed responses based on the content. \ | ||
| Make sure that their discussions strictly adhere to the context below and remains faithful to infomration in the context. \ |
There was a problem hiding this comment.
syntax: typo: 'infomration' should be 'information'
| Make sure that their discussions strictly adhere to the context below and remains faithful to infomration in the context. \ | |
| Make sure that their discussions strictly adhere to the context below and remains faithful to information in the context. \ |
| mind_problem_solving = """Convert the context below as a multi-turn problem-solving conversation where participants | ||
| analyze challenges or scenarios presented in the content and brainstorm solutions within the context of the provided | ||
| material, avoiding speculation or unrelated discussions. Make sure that their conversation strictly adhere to the | ||
| context below and remains faithful to infomration in the context. Please DONOT add any new information/reference other |
There was a problem hiding this comment.
syntax: typo: 'infomration' should be 'information'
| context below and remains faithful to infomration in the context. Please DONOT add any new information/reference other | |
| context below and remains faithful to information in the context. Please DONOT add any new information/reference other |
| mind_layman_knowall = """Imagine you are presenting the content below step-by-step to a layman. While you are presenting, | ||
| the layman has a lot of followup questions regarding your presentation. You answer the questions step-by-step with chain-of-thoughts. | ||
| Design this interaction between you and the layman as a multi-turn conversational manner. \ | ||
| Make sure that the interaction strictly adhere to the context below and remains faithful to infomration in the context. \ |
There was a problem hiding this comment.
syntax: typo: 'infomration' should be 'information'
| Make sure that the interaction strictly adhere to the context below and remains faithful to infomration in the context. \ | |
| Make sure that the interaction strictly adhere to the context below and remains faithful to information in the context. \ |
| mind_debate = """Convert the context below as a multi-turn debate-style conversation where the participants present arguments | ||
| and counterarguments based solely on the content provided, without introducing external information or personal opinions. Each | ||
| participant defends others arguments step-by-step with chain-of-thoughts. \ | ||
| Make sure that the conversation strictly adhere to the context below and remains faithful to infomration in the context. \ |
There was a problem hiding this comment.
syntax: typo: 'infomration' should be 'information'
| Make sure that the conversation strictly adhere to the context below and remains faithful to infomration in the context. \ | |
| Make sure that the conversation strictly adhere to the context below and remains faithful to information in the context. \ |
sarahyurick
left a comment
There was a problem hiding this comment.
Very nice, left a couple of high level comments for now. I am excited to test this out myself.
| ``` | ||
|
|
||
| ## Prerequisites | ||
| - GPU(s) with CUDA for the HF model |
There was a problem hiding this comment.
| - GPU(s) with CUDA for the HF model | |
| - GPU(s) with CUDA for the Hugging Face model |
| def inputs(self) -> tuple[list[str], list[str]]: | ||
|
|
||
| return ["data"], [self.text_field, self.n_tokens_field] |
There was a problem hiding this comment.
| def inputs(self) -> tuple[list[str], list[str]]: | |
| return ["data"], [self.text_field, self.n_tokens_field] | |
| def inputs(self) -> tuple[list[str], list[str]]: | |
| return ["data"], [self.text_field, self.n_tokens_field] |
| p.add_stage( | ||
| Modify(modifier_fn=fill_null_text, input_fields="text", output_fields="text").with_( | ||
| resources=Resources(cpus=0.5) | ||
| ) | ||
| ) |
There was a problem hiding this comment.
I am curious where some of these resource assignments are coming from. I belive Modify is probably better with cpus=0.5, did you set this because that's what you were observing?
There was a problem hiding this comment.
The resource assignment for this and other tasks was initially set to 0.5 CPUs to accommodate local development environment constraints. I've now increased it to 1 CPU across the examples, pending further performance testing. I believe it its useful to keep this setting explicit in the example so users can easily adjust the CPU allocation to match their specific infrastructure needs, thoughts?
There was a problem hiding this comment.
Got it, that's ok with me.
| model_name=model, | ||
| text_field="text", | ||
| max_length_tokens=chunk_length, | ||
| ).with_(resources=Resources(cpus=1)) |
There was a problem hiding this comment.
The default is 1 CPU, so with_ is not needed here.
| min_p=min_p, | ||
| max_tokens=max_tokens, | ||
| cache_dir=cache_dir, | ||
| ).with_(resources=Resources(cpus=1, gpus=1)) |
There was a problem hiding this comment.
It looks like this is the default already, so with_ can be removed.
| "finemath_classifier_tokenizer": {"resources": Resources(cpus=0.5)}, | ||
| "finemath_classifier_model": {"resources": Resources(cpus=1, gpus=1)}, |
There was a problem hiding this comment.
I think these are already the defaults, or should be overridden in their class definitions instead of the user needing to set them here.
There was a problem hiding this comment.
Additional Comments (1)
-
nemo_curator/stages/text/download/common_crawl/download.py, line 52 (link)logic: inconsistent dependency checking - uses
self._check_s5cmd_installed()but helper function is_check_s5cmd_installed(). Should this be calling the global helper function?
30 files reviewed, 3 comments
| logger.error(f"Failed to process {name}: {e}") | ||
| # We might want to continue to next dataset or stop here | ||
| # raise e |
There was a problem hiding this comment.
style: silently continues on subprocess failures which could mask critical issues - consider adding option to fail fast
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| sampling_params=self._sampling_params, | ||
| use_tqdm=False, | ||
| ) | ||
| return [out.outputs[0].text for out in outputs] |
There was a problem hiding this comment.
logic: assumes outputs[0] exists without checking - could fail if vLLM returns empty outputs array
There was a problem hiding this comment.
Added a check to see if outputs are empty before indexing
f23aaea to
e3e7856
Compare
| "input": "/lustre/fsw/portfolios/llmservice/users/rkarimimahab/data/finemath/original-data/finemath-4plus/*.parquet", | ||
| "output": "finemath_4_processed", | ||
| "fetch_cc": True, | ||
| "columns": { | ||
| "warc_filename": "warc_filename", | ||
| "offset": "warc_record_offset", | ||
| "length": "warc_record_length", | ||
| }, | ||
| }, | ||
| "FINEMATH_3": { | ||
| "input": "/lustre/fsw/portfolios/llmservice/users/rkarimimahab/data/finemath/original-data/finemath-3plus/*.parquet", | ||
| "output": "finemath_3_processed", | ||
| "fetch_cc": True, | ||
| "columns": { | ||
| "warc_filename": "warc_filename", | ||
| "offset": "warc_record_offset", | ||
| "length": "warc_record_length", | ||
| }, | ||
| }, | ||
| "OWM": { | ||
| "input": "hf://open-web-math/open-web-math", | ||
| # "input": "/home/sasatheesh/data/20t/jsonls/nv-math/open-web-math/*.parquet", # Local path alternative | ||
| "output": "owm_processed", | ||
| "fetch_cc": True, | ||
| "columns": { | ||
| "warc_filename": "warc_filename", | ||
| "offset": "warc_record_offset", | ||
| "length": "warc_record_length", | ||
| }, | ||
| }, | ||
| "INFIWEBMATH_4": { | ||
| "input": "/lustre/fsw/portfolios/llmservice/users/rkarimimahab/data/finemath/original-data/infiwebmath-4plus/*.parquet", | ||
| "output": "infiwebmath_4_processed", | ||
| "fetch_cc": True, | ||
| "columns": { | ||
| "warc_filename": "warc_filename", | ||
| "offset": "warc_record_offset", | ||
| "length": "warc_record_length", | ||
| }, | ||
| }, | ||
| "INFIWEBMATH_3": { | ||
| "input": "/lustre/fsw/portfolios/llmservice/users/rkarimimahab/data/finemath/original-data/infiwebmath-3plus/*.parquet", | ||
| "output": "infiwebmath_3_processed", | ||
| "fetch_cc": True, | ||
| "columns": { | ||
| "warc_filename": "warc_filename", | ||
| "offset": "warc_record_offset", | ||
| "length": "warc_record_length", | ||
| }, | ||
| }, | ||
| "MEGAMATH_PRO": { | ||
| "input": "/lustre/fsw/portfolios/llmservice/projects/llmservice_fm_text/adlr-stem/megamath_dataset/megamath-web-pro/*.parquet", | ||
| "output": "megamath_pro_processed", | ||
| "fetch_cc": True, | ||
| "columns": { | ||
| "warc_filename": "warc_filename", | ||
| "offset": "warc_record_offset", | ||
| "length": "warc_record_length", | ||
| }, | ||
| }, | ||
| "MEGAMATH_WEB": { | ||
| "input": "/lustre/fsw/portfolios/llmservice/projects/llmservice_fm_text/adlr-stem/megamath_dataset/megamath-web/**/*.parquet", |
There was a problem hiding this comment.
style: hardcoded NVIDIA internal paths make this non-portable for external users - consider adding configuration file or environment variable support
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| parser.add_argument("--input", required=True, help="Input directory or glob pattern for JSONL/Parquet files") | ||
| parser.add_argument("--output", required=True, help="Output directory for cleaned JSONL files") | ||
| parser.add_argument("--model", required=True, help="Model identifier (e.g., microsoft/phi-4)") | ||
| parser.add_argument("--prompt", required=True, help="Prompt name from prompts module (e.g., HTML_TO_TEXT_PROMPT)") |
There was a problem hiding this comment.
style: example references undefined HTML_TO_TEXT_PROMPT - should provide actual prompt name or indicate it's a placeholder
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| return result.stdout | ||
| else: | ||
| # Only log warning on failure | ||
| logger.warning(f"Failed to fetch WARC record {filename} at {offset}: {result.stderr.decode('utf-8')}") |
There was a problem hiding this comment.
logic: logger is not imported but used here, will cause NameError at runtime
| logger.warning(f"Failed to fetch WARC record {filename} at {offset}: {result.stderr.decode('utf-8')}") | |
| from loguru import logger |
(add this import at the top of the file)
| self.top_k = top_k | ||
| self.min_p = min_p | ||
| self.max_tokens = max_tokens | ||
| self.cache_dir = cache_dir |
There was a problem hiding this comment.
style: cache_dir parameter is stored but never passed to LLM initialization - consider using it or removing the parameter
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
+1 do we want to add it to setup/setup_on_node?
There was a problem hiding this comment.
Yes, absolutely! The cache_dir parameter should be passed to the vLLM LLM initialization in the setup method. Here's the fix:
| self.cache_dir = cache_dir | |
| llm_kwargs: dict[str, Any] = { | |
| "model": self.model, | |
| "enforce_eager": False, | |
| "trust_remote_code": True, | |
| } | |
| if final_max_model_len is not None: | |
| llm_kwargs["max_model_len"] = final_max_model_len | |
| if self.cache_dir is not None: | |
| llm_kwargs["download_dir"] = self.cache_dir | |
| if model_spec: | |
| llm_kwargs["tensor_parallel_size"] = model_spec.tensor_parallel_size | |
| llm_kwargs["max_num_batched_tokens"] = model_spec.max_num_batched_tokens | |
| self._llm = LLM(**llm_kwargs) |
The vLLM LLM constructor uses download_dir parameter to specify where model weights should be cached/downloaded, which corresponds to the cache_dir parameter in your wrapper.
There was a problem hiding this comment.
Additional Comments (1)
-
nemo_curator/stages/text/download/common_crawl/download.py, line 52 (link)logic: _check_s5cmd_installed() should be the global function, not a method
30 files reviewed, 1 comment
4a84490 to
45ce559
Compare
| --prompt HTML_TO_TEXT_PROMPT \ | ||
| --chunk_data \ | ||
| --chunk_length 5000 \ | ||
| --input_filetype parquet |
There was a problem hiding this comment.
syntax: --input_filetype parquet conflicts with line 128 which states input is JSONL files from Step 3
| --input_filetype parquet | |
| --input_filetype jsonl |
| --prompt HTML_TO_TEXT_PROMPT_CODE \ | ||
| --chunk_data \ | ||
| --chunk_length 5000 \ | ||
| --input_filetype parquet |
There was a problem hiding this comment.
syntax: --input_filetype parquet specified but the flow suggests JSONL files should be used at this step
| --input_filetype parquet | |
| --input_filetype jsonl |
| _MODELS: dict[str, ModelSpec] = { | ||
| "microsoft/phi-4": ModelSpec("microsoft/phi-4", 16_384, 1, 8_192, 128), | ||
| "google/gemma-3-12b-it": ModelSpec("google/gemma-3-12b-it", 131_072, 1, 8_192, 128), | ||
| "google/gemma-3-27b-it": ModelSpec("google/gemma-3-27b-it", 131_072, 2, 4096, 64), | ||
| "google/gemma-3-27b-it-32k": ModelSpec("google/gemma-3-27b-it", 32_768, 2, 4096, 64), | ||
| "Qwen/Qwen2-7B-Instruct": ModelSpec("Qwen/Qwen2-7B-Instruct", 131_072, 1, 8_192, 128), | ||
| "Qwen/Qwen2-72B-Instruct": ModelSpec("Qwen/Qwen2-72B-Instruct", 131_072, 4, 4096, 64), | ||
| "mistralai/Mistral-7B-Instruct-v0.3": ModelSpec( | ||
| "mistralai/Mistral-7B-Instruct-v0.3", 32768, 1, 8_192, 128 | ||
| ), | ||
| "deepseek-ai/DeepSeek-V3": ModelSpec("deepseek-ai/DeepSeek-V3", 163840, 8, 4096, 64), | ||
| "Qwen/Qwen2.5-32B-Instruct": ModelSpec("Qwen/Qwen2.5-32B-Instruct", 32_768, 8, 4096, 64), | ||
| "Qwen/Qwen2.5-72B-Instruct": ModelSpec("Qwen/Qwen2.5-72B-Instruct", 131_072, 8, 4096, 64), | ||
| "Qwen/Qwen3-30B-A3B": ModelSpec("Qwen/Qwen3-30B-A3B", 32_768, 2, 8_192, 64), | ||
| "Qwen/Qwen3-30B-A3B-Instruct-2507": ModelSpec( | ||
| "Qwen/Qwen3-30B-A3B-Instruct-2507", 262144, 4, 4096, 32 | ||
| ), | ||
| "openai/gpt-oss-20b": ModelSpec("openai/gpt-oss-20b", 131_072, 1, 8_192, 128), | ||
| "openai/gpt-oss-120b": ModelSpec("openai/gpt-oss-120b", 131_072, 4, 8_192, 64), | ||
| "Qwen/Qwen3-Coder-30B-A3B-Instruct": ModelSpec( | ||
| "Qwen/Qwen3-Coder-30B-A3B-Instruct", 262144, 4, 8_192, 256 | ||
| ), | ||
| "nvidia/NVIDIA-Nemotron-Nano-9B-v2": ModelSpec( | ||
| "nvidia/NVIDIA-Nemotron-Nano-9B-v2", 131_072, 1, 8_192, 128 | ||
| ), | ||
| } |
There was a problem hiding this comment.
Should we populate these using AutoConfig, etc. instead of hardcoding?
There was a problem hiding this comment.
I agree. I've removed these in the latest commit.
Some points to note:
- The ModelSpec consisted of model_name, max_model_len, tensor_parallel_size, max_num_batched_tokens and batch_size, in that order
- Only the max_model_len can be fetched from AutoConfig.
- for tensor_parallel_size, I've added a function that gets the GPU count and sets tp_size as the lowest power of 2 less than or equal to gpu_count.
- I've changed max_num_batched_tokens to a kwarg and set to a default of 4096, lower of 4096 and 8192.
- batch_size was an unused param so I removed it
| self.top_k = top_k | ||
| self.min_p = min_p | ||
| self.max_tokens = max_tokens | ||
| self.cache_dir = cache_dir |
There was a problem hiding this comment.
+1 do we want to add it to setup/setup_on_node?
| except (TypeError, AttributeError, ValueError): | ||
| return False | ||
|
|
||
| def _get_text_mime_types(self) -> set[str]: |
There was a problem hiding this comment.
We could consider moving this function (and the functions below it) to a separate utility file.
| CC_BASE_URL = "https://data.commoncrawl.org/" | ||
|
|
||
|
|
||
| def _check_s5cmd_installed() -> bool: |
There was a problem hiding this comment.
I see this function is originally defined for DocumentDownloader and used for Common Crawl and ArXiv. Can you move it to a shared script (maybe https://github.com/NVIDIA-NeMo/Curator/blob/main/nemo_curator/stages/text/download/utils.py)?
| length = int(row[self.warc_record_length_col]) | ||
|
|
||
| # Build the URL | ||
| url = f"{CC_BASE_URL}{filename}" |
There was a problem hiding this comment.
Perhaps urllib would be a safer way to do this.
| def test_inputs_outputs_cleanup_mode(self): | ||
| """Test inputs and outputs methods in cleanup mode.""" | ||
| stage = LLMCleanupStage(model="test-model", system_prompt="Clean: {text}") | ||
|
|
||
| inputs = stage.inputs() | ||
| outputs = stage.outputs() | ||
|
|
||
| assert inputs == (["data"], ["text", "n_tokens"]) | ||
| assert outputs == (["data"], ["cleaned_text"]) | ||
|
|
||
| def test_inputs_outputs_classification_mode(self): | ||
| """Test inputs and outputs methods in classification mode.""" | ||
| stage = LLMCleanupStage(model="test-model", system_prompt="Classify: {text}", classification=True) | ||
|
|
||
| inputs = stage.inputs() | ||
| outputs = stage.outputs() | ||
|
|
||
| assert inputs == (["data"], ["text", "n_tokens"]) | ||
| assert outputs == (["data"], ["label"]) |
| def test_setup_initializes_llm(self): | ||
| """Test that setup method initializes LLM and sampling params.""" | ||
| stage = LLMCleanupStage(model="test-model", system_prompt="Clean: {text}") | ||
|
|
||
| stage.setup() | ||
|
|
||
| assert stage._model._llm is not None | ||
| assert stage._model._sampling_params is not None | ||
| assert isinstance(stage._model._llm, MockLLM) | ||
| assert isinstance(stage._model._sampling_params, MockSamplingParams) | ||
|
|
||
| def test_setup_qwen3_model(self): | ||
| """Test setup method with Qwen3 model (special sampling params).""" | ||
| stage = LLMCleanupStage(model="Qwen/Qwen3-30B-A3B", system_prompt="Clean: {text}", top_k=10, min_p=0.1) | ||
|
|
||
| stage.setup() | ||
|
|
||
| assert stage._model._sampling_params is not None | ||
| # Qwen3 models should have top_k and min_p set | ||
| assert hasattr(stage._model._sampling_params, "top_p") | ||
| assert hasattr(stage._model._sampling_params, "top_k") | ||
| assert hasattr(stage._model._sampling_params, "min_p") | ||
|
|
||
| def test_setup_non_qwen3_model(self): | ||
| """Test setup method with non-Qwen3 model.""" | ||
| stage = LLMCleanupStage(model="microsoft/phi-4", system_prompt="Clean: {text}") | ||
|
|
||
| stage.setup() | ||
|
|
||
| assert stage._model._sampling_params is not None | ||
| # Non-Qwen3 models should only have top_p | ||
| assert hasattr(stage._model._sampling_params, "top_p") |
| def test_setup_with_cache_dir(self): | ||
| """Test setup method with cache_dir specified.""" | ||
| stage = LLMCleanupStage(model="test-model", system_prompt="Clean: {text}", cache_dir="/test/cache") | ||
|
|
||
| stage.setup() | ||
|
|
||
| assert stage._model._llm is not None | ||
| assert stage._model.cache_dir == "/test/cache" | ||
|
|
||
| def test_setup_with_max_tokens(self): | ||
| """Test setup method with max_tokens specified.""" | ||
| stage = LLMCleanupStage(model="test-model", system_prompt="Clean: {text}", max_tokens=500) | ||
|
|
||
| stage.setup() | ||
|
|
||
| assert stage._model._sampling_params is not None | ||
| assert stage._model._sampling_params.max_tokens == 500 | ||
|
|
||
| def test_setup_max_tokens_fallback(self): | ||
| """Test setup method falls back to max_model_len when max_tokens is None.""" | ||
| stage = LLMCleanupStage( | ||
| model="test-model", system_prompt="Clean: {text}", max_model_len=16000, max_tokens=None | ||
| ) | ||
|
|
||
| stage.setup() | ||
|
|
||
| assert stage._model._sampling_params is not None | ||
| assert stage._model._sampling_params.max_tokens == 16000 |
| # ========================================================================= | ||
| "FINEMATH_4PLUS": { | ||
| # HuggingFace: hf://HuggingFaceTB/finemath/finemath-4plus | ||
| "input": "/lustre/fsw/portfolios/llmservice/users/rkarimimahab/data/finemath/original-data/finemath-4plus/*.parquet", |
There was a problem hiding this comment.
These inputs aren't accessible for non-Nvidians though, right?
There was a problem hiding this comment.
These will need to be paths to the datasets downloaded locally. @vikalluru - Think these have moved to datasets.json but still applies
| Extract and preprocess text from raw web data: | ||
|
|
||
| ```bash | ||
| python tutorials/math/run_text_preprocess.py \ |
There was a problem hiding this comment.
What do you think about renaming the scripts to 1_run_text_preprocess.py, 2_run_quality_classifier.py, etc. to better represent the order these scripts are run?
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
|
/ok to test dab732d |
sarahyurick
left a comment
There was a problem hiding this comment.
It looks like there are some merge conflicts that need to be resolved.
As a follow-up, we will need to add benchmarks under https://github.com/NVIDIA-NeMo/Curator/tree/main/benchmarking too.
After LLM cleanup, documents are split across multiple rows (one per chunk). This adds a merge stage that deduplicates, filters invalid chunks (NO USEFUL CONTENT, empty, null), sorts by chunk_id, and concatenates text back into one row per document. Token count columns are summed; metadata columns use first(). Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
ChunkMergeStage (Step 6) — post-LLM chunk reassembly. After LLM cleanup splits documents into multiple rows per chunk, this stage merges them back into one row per document by deduplicating, filtering invalid chunks, sorting by chunk_id, and concatenating text. Token counts are summed; metadata columns preserved via first(). New files: - nemo_curator/stages/math/modifiers/merge_chunks.py - tests/stages/math_stages/modifiers/test_merge_chunks.py (11 tests) - tutorials/math/6_postprocess.py Signed-off-by: Sukrit Rao <sukritr@ndia.com>
- Update copyright headers from 2025 to 2026 across 18 math pipeline
- Fix off-by-one bug in CenterCropTextStage._mid_slice where len(s) - 1
truncated the last character; updated tests to expect correct behavior
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
- Merge 5_llm_cleanup.py + 6_postprocess.py into 3_llm_cleanup.py with
ChunkMergeStage as an inline pipeline stage
- Renumber: quality classifier → step 4, deduplication → step 5
- Delete old scripts (3-6) replaced by new numbering
- Update README: diagram, summary table, step sections, script references
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
- Fix dedup check: use FuzzyDuplicateIds (conditional) not fuzzy_id_generator.json (always written) - Add MathExtractStage dataclass to extract.py; update tutorials + tests to use it - Fix stale import in 3_llm_cleanup.py (modifiers path moved in upstream merge) - Add try/finally around ray_client.stop() in 2_text_preprocess.py and 4_quality_classifier.py Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
- Use local_files_only=True and self.model_name in math stage setup - Align download tests with upstream (DocumentIterateExtractStage, wget retry flags, copyright years) Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
Additional Comments (3)
The This inconsistency means that for other Qwen3 thinking models (e.g., Consider aligning the check with the one in is_qwen3_thinking = "Qwen3" in self.model_name or "qwen3" in self.model_name.lower()
is_qwen3_30b_a3b = self.model_name == "Qwen/Qwen3-30B-A3B"
# Apply /no_think for all Qwen3 thinking models (or at minimum match VLLMModel's check)
Item 6 in The same fix applies to
When all rows fail extraction, Compare this with output_cols = self.extractor.output_columns()
if self.filename_col:
output_cols = [*output_cols, self.filename_col]
return DocumentBatch(
task_id=batch.task_id,
dataset_name=batch.dataset_name,
data=pd.DataFrame(records) if records else pd.DataFrame(columns=output_cols),
_metadata=batch._metadata,
_stage_perf=batch._stage_perf,
) |
|
/ok to test 1a5c645 |
- Move vLLM initialization from setup() to setup_on_node() to prevent torch.compile cache race condition with concurrent workers (NVIDIA-NeMo#1514) - Broaden Qwen3 /no_think check to all Qwen3 models; use enable_thinking=False for Qwen3.5+ which dropped the prompt switch - Preserve DataFrame schema on empty extraction batches - Close unclosed ** bold markdown in prompts Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
|
/ok to test 66b7192 |
Additional Comments (3)
Move session creation to a import threading
def __init__(self, ...):
...
self._session = None
self._session_lock = threading.Lock()
def _get_session(self) -> requests.Session:
if self._session is None:
with self._session_lock:
if self._session is None:
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=self.max_workers,
pool_maxsize=self.max_workers * 2,
max_retries=self.max_retries,
)
session.mount("https://", adapter)
session.mount("http://", adapter)
self._session = session
return self._session
If
Pass def get_max_model_len_from_config(model: str, cache_dir: str | None = None) -> int | None:
config = AutoConfig.from_pretrained(model, trust_remote_code=True, cache_dir=cache_dir)
...And in final_max_model_len = get_max_model_len_from_config(self.model, cache_dir=self.cache_dir) |
sarahyurick
left a comment
There was a problem hiding this comment.
Blocking this PR temporarily, until @raosukrit67 adds additional changes as discussed.
- Fix enable_thinking not reaching Qwen3 chat template (was nested in dict instead of passed as top-level kwarg) - Add @Property to VLLMModel.model_id_names to match base class - Add --text-field arg to quality classifier for post-cleanup use - Add output_filetype to dedup workflow to preserve JSONL format - Error handling in gpu_utils: raise on zero GPUs, try/except for AutoConfig, pass cache_dir through - Always define _model_kwargs in LLMCleanupStage to prevent AttributeError after serialization - Make chunk split/merge guards consistent in LLM cleanup tutorial - Make lynx tests run without lynx by mocking shutil.which - Thread-safe lazy init in MathContentExtractor - Update tests for np.ndarray returns and removed autocast param Signed-off-by: Sukrit Rao <sukritr@nvidia.com>
|
/ok to test 3171358 |
Additional Comments (3)
In Consider adding def __init__(self, ..., cache_dir: str | None = None):
...
self.cache_dir = cache_dir
def setup_on_node(self, ...) -> None:
snapshot_download(repo_id=self.model_name, cache_dir=self.cache_dir, local_files_only=False)
def setup(self, ...) -> None:
self._tokenizer = AutoTokenizer.from_pretrained(
self.model_name, cache_dir=self.cache_dir, local_files_only=True
)
Chunks produced by
This results in three consecutive newlines between merged chunks, creating an extra blank line compared to the original document's paragraph spacing ( Consider either:
For Qwen3 (non-Qwen3.5+) models, three separate mechanisms are applied simultaneously to disable chain-of-thought:
Consider removing the |
Description
Implements extraction and classification of mathematical content as the starting point for a math pipeline.
Usage
See examples/math/README.md
Checklist