Skip to content

Fix numpy reader returning unread sample data - #6482

Open
jantonguirao wants to merge 1 commit into
NVIDIA:mainfrom
jantonguirao:fix/numpy-reader-deferred-read
Open

Fix numpy reader returning unread sample data#6482
jantonguirao wants to merge 1 commit into
NVIDIA:mainfrom
jantonguirao:fix/numpy-reader-deferred-read

Conversation

@jantonguirao

Copy link
Copy Markdown
Collaborator

Category:

Bug fix (non-breaking change which fixes an issue)

Description:

fn.readers.numpy could hand out sample buffers that were never read. No error was raised: the
operator reported the correct shape and dtype, and the read was reported as full-length, but the
contents were whatever happened to be in the freshly allocated buffer - zeros, or stale heap data.

Two places decide whether the sample data still needs reading, and they disagreed.

NumpyLoader::ReadSample defers the read based on the actual stream:

if (!opts.use_mmap || !current_file->CanMemoryMap()) {
  target.current_file = std::move(current_file);   // data NOT read yet; read it later
} else {
  auto p = current_file->Get(nbytes);              // mmap path; data available now
  ...
}

NumpyReaderCPU::Prefetch, which performs that deferred read, gated on the user-facing argument
instead:

if (!dont_use_mmap_)
  return;                                          // skips the deferred read entirely

So when the stream was not memory mappable but dont_use_mmap was not requested, the loader
deferred the read and the operator never performed it.

This affects every s3:// path, because S3FileStream does not override CanMemoryMap() and so is
never mappable, and local files whenever the mmap reservation cannot be satisfied - copy_read_data_
is dont_use_mmap_ || !mmap_reserver_.CanShareMappedData().

The fix drops the early return. The loop below already skips samples that need no deferred read, and
in the memory mapped case that is all of them, so behaviour there is unchanged. use_o_direct is
unaffected; it already requires dont_use_mmap.

Additional information:

Affected modules and functionalities:

  • dali/operators/reader/numpy_reader_op.cc: NumpyReaderCPU::Prefetch no longer returns early.
  • dali/test/python/reader/test_numpy.py: new regression test.

Key points relevant for the review:

  • Worth confirming the break on !target->current_file is the behaviour we want for a batch that
    mixes mappable and non-mappable sources - it is pre-existing, and reachable today via files=
    with a mix of local and s3:// paths, but this change does not alter it.
  • The regression test reproduces the bug on a plain local filesystem by asking for an initial_fill
    above the max_map_count / 2 limit, which makes the mmap reservation fail. initial_fill only
    feeds initial_buffer_fill_ when random_shuffle is on. It uses a single tiny sample so the
    shuffle buffer fill stays cheap (about 1 s) and shuffling cannot change which sample comes back.
  • Verified by rebuilding the same tree three times and changing only this file: with the fix the
    test passes, with the file reverted it fails with an all-zero array, and with the fix restored it
    passes again.

Tests:

  • Existing tests apply
  • New tests added
    • Python tests
    • GTests
    • Benchmark
    • Other
  • N/A

test_numpy.py: test_sample_data_is_read_when_mmap_is_unavailable

Checklist

Documentation

  • Existing documentation applies
  • Documentation updated
  • N/A

DALI team only

Requirements

  • Implements new requirements
  • Affects existing requirements
  • N/A

REQ IDs: N/A

JIRA TASK: DALI-4887

NumpyLoader::ReadSample defers reading the sample data whenever the stream
cannot be memory mapped, leaving it to NumpyReaderCPU::Prefetch. The two
disagreed on when that happens: the loader decides from the stream itself
(`!opts.use_mmap || !current_file->CanMemoryMap()`), while Prefetch returned
early unless the user passed dont_use_mmap.

When the two disagreed the deferred read never happened and the sample was
handed out as a freshly allocated, never-written buffer - zeros, or stale heap
contents - with the correct shape and dtype and no error. This affects every
s3:// path, because S3FileStream is never memory mappable, and local files
whenever the mmap reservation cannot be satisfied.

Drop the early return. The loop below already skips samples that need no
deferred read, and in the memory mapped case that is all of them, so the
behaviour there is unchanged. use_o_direct is unaffected; it already requires
dont_use_mmap.

Add a regression test that exhausts the mmap reservation locally by asking for
an initial_fill above the max_map_count / 2 limit.
Copilot AI lite review requested due to automatic review settings September 11, 2026 18:23
@copy-pr-bot

copy-pr-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Copilot AI 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.

🟡 Changes recommended

Unresolved critical and moderate findings remain.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Fixes the NumPy reader returning unread sample data when memory mapping is unavailable.

Changes:

  • Performs deferred reads for non-mappable samples.
  • Adds a regression test for mmap fallback behavior.
File summaries
File Review summary
dali/operators/reader/numpy_reader_op.cc Critical: Mixed mapped and non-mappable batches can still leave deferred samples unread due to the existing break.
dali/test/python/reader/test_numpy.py Moderate: The test may create excessive file handles by scaling samples with the host max_map_count.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +266 to +270
// Read the samples that the loader could not provide by memory mapping. Note that this is not
// the same as `dont_use_mmap_`: NumpyLoader::ReadSample decides based on the actual stream
// (`!opts.use_mmap || !current_file->CanMemoryMap()`), so a sample can need a deferred read even
// when the user did not ask for `dont_use_mmap`, e.g. for remote storage, which is never
// mappable, or when the mmap reservation could not be satisfied. Samples that the loader already
Comment on lines +1239 to +1242
# FileLoader reserves initial_buffer_fill_ mappings up front and falls back to copying reads
# when that fails; the limit is max_map_count / 2 (mmaped_file.cc). initial_fill only feeds
# initial_buffer_fill_ when random_shuffle is on.
initial_fill = max_map_count // 2 + 1
@greptile-apps

greptile-apps Bot commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 4/5

The PR is not yet safe to merge because mixed local and non-mappable batches can still return unread samples, and the new test must also follow the repository's required pipeline-definition pattern.

Findings

  1. P1 Mixed batches remain unread
  2. P2 Legacy pipeline construction
  3. P2 Host-dependent test cost

Summary

  • Removes the incorrect early return from NumpyReaderCPU::Prefetch.
  • Adds a local regression test that forces mmap reservation failure.
  • The fix remains incomplete for batches mixing mapped and non-mappable streams because the loop stops at the first mapped sample.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Prefetch batch] --> B{Sample has current_file?}
  B -->|Yes: deferred stream| C[Read sample data]
  C --> D[Next sample]
  B -->|No: already mapped| E[Current code breaks loop]
  E --> F[Later deferred samples remain unread]
  D --> B
Loading

Reviews (1) · Last reviewed commit: "Fix numpy reader returning unread sample..."

Comment on lines +266 to +271
// Read the samples that the loader could not provide by memory mapping. Note that this is not
// the same as `dont_use_mmap_`: NumpyLoader::ReadSample decides based on the actual stream
// (`!opts.use_mmap || !current_file->CanMemoryMap()`), so a sample can need a deferred read even
// when the user did not ask for `dont_use_mmap`, e.g. for remote storage, which is never
// mappable, or when the mmap reservation could not be satisfied. Samples that the loader already
// provided leave `current_file` empty and are skipped below.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Mixed batches remain unread

When mmap is enabled, a batch can contain a mapped local sample followed by a deferred, non-mappable sample if files mixes local and S3 paths. The loop exits at the first mapped sample because its current_file is empty, so later remote samples are reset without being read and can produce empty or invalid output. Skip mapped samples individually while preserving the separate break for duplicate padding samples.

Comment on lines +1249 to +1255
pipe = Pipeline(batch_size=1, num_threads=1, device_id=None)
with pipe:
pipe.set_outputs(
fn.readers.numpy(
file_root=test_data_root, random_shuffle=True, initial_fill=initial_fill
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Legacy pipeline construction

This new test manually constructs a Pipeline and uses with pipe and set_outputs. The repository requires new tests to define pipelines with @pipeline_def, so this requirement must be satisfied before merging.

Suggested change
pipe = Pipeline(batch_size=1, num_threads=1, device_id=None)
with pipe:
pipe.set_outputs(
fn.readers.numpy(
file_root=test_data_root, random_shuffle=True, initial_fill=initial_fill
)
)
@pipeline_def(batch_size=1, num_threads=1, device_id=None)
def make_pipe():
return fn.readers.numpy(
file_root=test_data_root, random_shuffle=True, initial_fill=initial_fill
)
pipe = make_pipe()

Rule Used: New tests should use @pipeline_def and DALI_extra paths (via dali_extra_path() / DALI_EXTRA_PATH). Don't use the legacy ops. API in new tests, and don't hardcode personal data paths. (source)

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!

Comment on lines +1239 to +1242
# FileLoader reserves initial_buffer_fill_ mappings up front and falls back to copying reads
# when that fails; the limit is max_map_count / 2 (mmaped_file.cc). initial_fill only feeds
# initial_buffer_fill_ when random_shuffle is on.
initial_fill = max_map_count // 2 + 1

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Host-dependent test cost

The test derives initial_fill from the host's max_map_count, and loader initialization performs that many sample reads while repeatedly opening and parsing the only file. This means tens of thousands of operations on common hosts and potentially hundreds of thousands on hosts with a larger limit, making test duration depend heavily on system configuration. Use a bounded way to trigger the reservation failure instead.

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!

@jantonguirao

Copy link
Copy Markdown
Collaborator Author

!build

@dali-automaton

Copy link
Copy Markdown
Collaborator

CI MESSAGE: [67423401]: BUILD STARTED

@dali-automaton

Copy link
Copy Markdown
Collaborator

CI MESSAGE: [67423401]: BUILD FAILED

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.

3 participants