Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Parse a .json dataset file as one JSON document instead of line-by-line#7422

Open
vineethsaivs wants to merge 3 commits into
unslothai:mainunslothai/unsloth:mainfrom
vineethsaivs:fix/raw-text-json-document-parsingvineethsaivs/unsloth:fix/raw-text-json-document-parsingCopy head branch name to clipboard
Open

Parse a .json dataset file as one JSON document instead of line-by-line#7422
vineethsaivs wants to merge 3 commits into
unslothai:mainunslothai/unsloth:mainfrom
vineethsaivs:fix/raw-text-json-document-parsingvineethsaivs/unsloth:fix/raw-text-json-document-parsingCopy head branch name to clipboard

Conversation

@vineethsaivs

Copy link
Copy Markdown
Contributor

Summary

RawTextDataLoader maps both .json and .jsonl to the same "json_lines" reader (SUPPORTED_FORMATS), and that reader parses the file one line at a time:

elif file_format == "json_lines":
    lines = []
    for line in f:
        try:
            data = json.loads(line.strip())
            ...
        except json.JSONDecodeError:
            continue
    return "\n\n".join(lines)

A real .json file is a single JSON document, usually a top-level list of records (the common instruct-dataset shape) or a pretty-printed object spanning many lines. None of its individual lines are valid standalone JSON, so every json.loads(line) raises, all lines are skipped, and the reader returns "". load_from_file then rejects the perfectly valid file with ValueError: File '...' is empty or contains only whitespace. Only true line-delimited .jsonl worked.

Fix

Read the file and try json.loads on the whole content first: a list stays a list of records, a single object becomes one record. Fall back to the existing line-by-line parse for true .jsonl (a multi-line JSONL file fails the whole-file parse and drops through). Text extraction and the .jsonl path are otherwise unchanged.

Test

tests/test_raw_text_json_loading.py stubs datasets and execs raw_text.py directly (no import unsloth, so no GPU / unsloth_zoo), then checks that a .json array file and a .jsonl file both read back their records. The .json case fails before this change (the reader returns "") and passes after; the .jsonl case is a regression guard. Wired into the Bucket-A CPU CI step.

Both .json and .jsonl map to the "json_lines" reader, which parsed the
file one line at a time with json.loads(line). A real .json file is a
single JSON document (commonly a top-level list of records), so every
line failed to parse, the whole document was dropped, and the reader
returned an empty string; load_from_file then rejected the valid file
with "is empty or contains only whitespace".

Read the file and try json.loads on the whole content first (handling a
list or a single object), falling back to line-by-line for true .jsonl.
Add a CPU-only regression test.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d038c77f94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread unsloth/dataprep/raw_text.py Outdated
if file_format == "plain_text" or file_format == "markdown":
return f.read()
elif file_format == "json_lines":
content = f.read()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep JSONL parsing streaming

Because .jsonl still maps to this json_lines handler, this f.read() now materializes the entire raw JSONL file before falling back to the per-line parser; the fallback also accumulates every decoded record before extracting text. Large training JSONL files that the previous loop processed one row at a time can now run out of memory, especially when rows contain large non-text fields. Please keep the .jsonl path streaming, or only use whole-document parsing for .json files.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, that was a real regression. Reading the whole file up front to try the document parse pulled a large .jsonl into memory, and the fallback then held every decoded record too, so the streaming loop the original code had was lost.

Fixed in d13a4b5: whole-document parsing is now only reached for a .json file, and .jsonl goes straight to a generator.

elif file_format == "json_lines":
    if Path(file_path).suffix.lower() == ".json":
        # A .json file is a single JSON document (commonly a list
        # of records), so parsing it per line drops the whole file.
        try:
            parsed = json.load(f)
            records = parsed if isinstance(parsed, list) else [parsed]
        except json.JSONDecodeError:
            # Some files carry JSON Lines under a .json name.
            f.seek(0)
            records = self._iter_json_lines(f)
    else:
        # A .jsonl file is one JSON value per line: stay streaming so
        # a large file is never held in memory all at once.
        records = self._iter_json_lines(f)
    lines = []
    for data in records:
        text = self._extract_text_from_json(data)
        if text:
            lines.append(text)
    return "\n\n".join(lines)

_iter_json_lines yields one record at a time (skipping blank and malformed lines, same as the original loop), so on the .jsonl path nothing more than a single line is resident, and the records list no longer exists at all. json.load(f) on the .json path reads the document whole, which is unavoidable for a single JSON value, and that is the file shape this PR is about.

I kept the per-line fallback for .json, since files carrying JSON Lines under a .json name are common and they parsed fine before this PR. It only runs after a whole-document parse has already failed on a .json file, so it never costs the .jsonl path anything.

Added test_jsonl_is_never_materialized, which wraps the handle so read() and seek() raise, and it fails on the previous commit:

E   AssertionError: .jsonl was read whole instead of streamed line by line

Also added test_json_holding_json_lines_still_falls_back to pin the mislabelled-.json path. All 4 tests pass on the new commit.

The previous commit read the whole file before trying a whole-document parse,
so a large .jsonl file that used to be processed one row at a time was
materialised in memory, along with every decoded record.

Whole-document parsing is only needed for a .json file, so branch on the
extension: .json parses with json.load (with the per-line fallback kept for
files that carry JSON Lines under a .json name), and .jsonl goes straight to a
generator that yields one record per line and never holds the file.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Morty Proxy This is a proxified and sanitized view of the page, visit original site.