Skip to content

Refactored multicore summarisation to distribute pre-split data - #215

Open
tonywu1999 wants to merge 1 commit into
develfrom
MSstats/work/20260514_multicore-and-benchmark
Open

Refactored multicore summarisation to distribute pre-split data#215
tonywu1999 wants to merge 1 commit into
develfrom
MSstats/work/20260514_multicore-and-benchmark

Conversation

@tonywu1999

@tonywu1999 tonywu1999 commented Jul 22, 2026

Copy link
Copy Markdown
Contributor
  • MSstatsSummarizeWithMultipleCores: replaced protein_indices = split(seq_len(nrow(input)), ...) (each worker re-indexed into the broadcast input) with protein_data = split(input, ...) so each worker receives only its own protein's rows. Dropped input and protein_indices from clusterExport accordingly.
  • Removed the per-100-proteins progress log inside the worker function — counting completed proteins under parLapply without coordination is unreliable; the startup "Number of proteins to process: N" cat is sufficient.
  • Fixed an undefined-variable regression on the same change path: num_proteins = length(protein_indices) would have thrown object 'protein_indices' not found on the first multicore invocation. Now reads length(protein_data).
  • Added inst/tinytest/test_dataProcess.R multicore-parity block asserting single-core and multi-core linear ProteinLevelData agree on row count, protein set, LogIntensities, and Variance.
  • Added benchmark/profile_dataprocess_peak.R that drives dataProcess() under profmem::profmem() and reports peak RSS via ps::ps_memory_info().
  • DESCRIPTION: added callr, ps, profmem to Suggests so the benchmark script is runnable with a checkout. None become hard dependencies.

See MSstats-ai/todos/active/TODO-MS-20260514_fix-memory-bugs.md

Motivation and Context

Please include relevant motivation and context of the problem along with a short summary of the solution.

Changes

Please provide a detailed bullet point list of your changes.

Testing

Please describe any unit tests you added or modified to verify your changes.

Checklist Before Requesting a Review

  • I have read the MSstats contributing guidelines
  • My changes generate no new warnings
  • Any dependent changes have been merged and published in downstream modules
  • I have run the devtools::document() command after my changes and committed the added files

Motivation and context

Multicore summarisation previously broadcast the full input to workers and passed row indices, causing unnecessary memory usage and introducing an undefined protein_indices regression. Worker progress logging was also unreliable.

The implementation now distributes pre-split per-protein data directly to workers, reducing the exported payload and improving multicore reliability.

Changes

  • Refactored multicore summarisation to split input into per-protein datasets.
  • Passed protein datasets directly to summarisation workers instead of numeric indices.
  • Removed unnecessary exports of input and protein_indices.
  • Removed unreliable per-100-protein worker progress logging.
  • Updated protein counting to use protein_data.
  • Added benchmark/profile_dataprocess_peak.R for stage-by-stage memory profiling.
  • Added optional callr, ps, and profmem dependencies to Suggests.

Tests

  • Added multicore parity tests for dataProcess().
  • Compared single- and multi-core results for:
    • Row counts
    • Protein sets
    • LogIntensities
    • Variance, when available

Coding guideline violations

  • None identified.

* MSstatsSummarizeWithMultipleCores: replaced
  `protein_indices = split(seq_len(nrow(input)), ...)` (each worker
  re-indexed into the broadcast `input`) with
  `protein_data = split(input, ...)` so each worker receives only
  its own protein's rows. Dropped `input` and `protein_indices`
  from clusterExport accordingly.
* Removed the per-100-proteins progress log inside the worker
  function — counting completed proteins under parLapply without
  coordination is unreliable; the startup "Number of proteins to
  process: N" cat is sufficient.
* Fixed an undefined-variable regression on the same change path:
  `num_proteins = length(protein_indices)` would have thrown
  `object 'protein_indices' not found` on the first multicore
  invocation. Now reads `length(protein_data)`.
* Added inst/tinytest/test_dataProcess.R multicore-parity block
  asserting single-core and multi-core linear ProteinLevelData
  agree on row count, protein set, LogIntensities, and Variance.
* Added benchmark/profile_dataprocess_peak.R that drives
  dataProcess() under profmem::profmem() and reports peak RSS via
  ps::ps_memory_info().
* DESCRIPTION: added callr, ps, profmem to Suggests so the
  benchmark script is runnable with a checkout. None become hard
  dependencies.

See MSstats-ai/todos/active/TODO-MS-20260514_fix-memory-bugs.md

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Multicore protein summarization now pre-splits data before worker execution. A linear-summary parity test was added, and a standalone benchmark replays dataProcess stages to report peak memory. Suggested packages support the profiling workflow.

Changes

Data processing and profiling

Layer / File(s) Summary
Pre-split parallel summarization
R/dataProcess.R, inst/tinytest/test_dataProcess.R
Parallel workers receive per-protein data directly with a reduced export payload; multicore linear summaries are compared against single-core results.
Peak-memory profiling workflow
benchmark/profile_dataprocess_peak.R, DESCRIPTION
A configurable benchmark replays pipeline stages, records GC memory checkpoints, and adds profiling-related suggested packages.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related issues

Possibly related PRs

  • Vitek-Lab/MSstats#193 — Both changes adjust protein-based splitting in MSstatsSummarizeWithMultipleCores().

Suggested labels: Review effort 2/5

Suggested reviewers: mstaniak

Poem

A bunny split proteins in a neat little row,
Sent small bits to workers, not one giant load.
A memory meter hopped stage by stage,
While tests checked the answers on every page.
“Less clutter!” cried Rabbit, and bounded away.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description has useful change details but is missing the required filled-out Motivation, Testing, and checklist sections. Add a filled Motivation and Context section, a Testing section, and complete the checklist items; keep the changes as a concise bullet list.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: multicore summarisation now uses pre-split data.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch MSstats/work/20260514_multicore-and-benchmark

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Failed to generate code suggestions for PR

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@benchmark/profile_dataprocess_peak.R`:
- Around line 115-122: The checkpoint function currently reports only R Vcells
usage and must collect the promised broader memory metrics. Update checkpoint
and its stage instrumentation to record process RSS via ps and largest
allocations via profmem, using callr where isolation or monitoring is needed;
otherwise narrow the script’s peak-memory claims to Vcells only.

In `@inst/tinytest/test_dataProcess.R`:
- Around line 126-148: Update the comparison block after sorting linear_single
and linear_multi to assert matching as.character(RUN) values alongside Protein
before comparing LogIntensities or Variance; when LABEL exists in both
summaries, assert matching LABEL values as well. Keep the existing ordering and
numeric comparisons unchanged.

In `@R/dataProcess.R`:
- Around line 240-253: Register an on.exit cleanup immediately after creating
the cluster used by the summarized_results parallel branches, ensuring
parallel::stopCluster(cl) runs even when either parallel::parLapply call fails.
Preserve the existing successful execution and outer tryCatch behavior.
- Around line 246-252: Update both the parallel `MSstatsSummarizeSingleLinear`
call inside `parallel::parLapply` and the single-core call in the same
data-processing flow to forward the exported `equal_variance` value as the
summarizer’s `equalFeatureVar` argument. Add coverage exercising
`equalFeatureVar = FALSE` and confirming matching behavior between parallel and
single-core paths.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 427804df-b1e5-4276-b082-a0e322d5ee83

📥 Commits

Reviewing files that changed from the base of the PR and between 68bc8a8 and 25dcfac.

📒 Files selected for processing (4)
  • DESCRIPTION
  • R/dataProcess.R
  • benchmark/profile_dataprocess_peak.R
  • inst/tinytest/test_dataProcess.R

Comment on lines +115 to +122
checkpoint <- function(label, reset = FALSE) {
if (reset) { gc(); gc(reset = TRUE) }
g <- gc()
cols <- colnames(g)
used_mb <- g["Vcells", which(cols == "used")[1] + 1]
max_mb <- g["Vcells", which(cols == "max used")[1] + 1]
cat(sprintf("%-40s used=%6.1f max=%6.1f\n", label, used_mb, max_mb))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Collect the promised process and allocation memory measurements.

This reports only R’s Vcells high-water mark. It never calls ps, profmem, or callr, so it misses RSS/external allocations and the largest allocation despite the new DESCRIPTION dependencies and profiling objective. Instrument the stages with profmem and capture process memory via ps (using callr if isolation/monitoring is required), or narrow the script’s peak-memory claims.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@benchmark/profile_dataprocess_peak.R` around lines 115 - 122, The checkpoint
function currently reports only R Vcells usage and must collect the promised
broader memory metrics. Update checkpoint and its stage instrumentation to
record process RSS via ps and largest allocations via profmem, using callr where
isolation or monitoring is needed; otherwise narrow the script’s peak-memory
claims to Vcells only.

Comment on lines +126 to +148
# Sort both by (Protein, RUN) so the comparison is order-independent
linear_single = QuantDataDefaultLinear$ProteinLevelData
linear_multi = QuantDataParallelLinear$ProteinLevelData
linear_single = linear_single[order(as.character(linear_single$Protein),
as.character(linear_single$RUN)), ]
linear_multi = linear_multi[order(as.character(linear_multi$Protein),
as.character(linear_multi$RUN)), ]
rownames(linear_single) = NULL
rownames(linear_multi) = NULL

expect_equal(as.character(linear_single$Protein),
as.character(linear_multi$Protein),
info = "Linear multicore should cover the same set of proteins")

expect_equal(linear_single$LogIntensities,
linear_multi$LogIntensities,
info = "Linear multicore LogIntensities should match single-core")

if ("Variance" %in% colnames(linear_single) &&
"Variance" %in% colnames(linear_multi)) {
expect_equal(linear_single$Variance,
linear_multi$Variance,
info = "Linear multicore Variance should match single-core")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert RUN identity before comparing summaries.

Line 136 checks only Protein. Compare as.character(RUN) as well (and LABEL when applicable) before checking LogIntensities and Variance; sorting alone does not prove the aligned rows represent the same runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@inst/tinytest/test_dataProcess.R` around lines 126 - 148, Update the
comparison block after sorting linear_single and linear_multi to assert matching
as.character(RUN) values alongside Protein before comparing LogIntensities or
Variance; when LABEL exists in both summaries, assert matching LABEL values as
well. Keep the existing ordering and numeric comparisons unchanged.

Comment thread R/dataProcess.R
Comment on lines +240 to 253
summarized_results = parallel::parLapply(cl, protein_data, function(single_protein) {
MSstatsSummarizeSingleTMP(
single_protein, impute, censored_symbol, remove50missing,
aft_iterations)
})
} else {
summarized_results = parallel::parLapply(cl, seq_len(num_proteins), function(i) {
if (i %% 100 == 0) {
cat("Finished processing an additional 100 proteins",
sep = "\n", file = "MSstats_dataProcess_log_progress.log", append = TRUE)
}
single_protein = input[protein_indices[[i]],]
summarized_results = parallel::parLapply(cl, protein_data, function(single_protein) {
MSstatsSummarizeSingleLinear(
single_protein,
impute,
censored_symbol,
impute,
censored_symbol,
remove50missing,
aft_iterations)
})

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Always stop the cluster on worker failure.

If either parLapply() call errors, Line 255 is skipped. The outer tryCatch() converts that failure to NULL, but the PSOCK workers remain alive. Register on.exit(parallel::stopCluster(cl), add = TRUE) immediately after cluster creation.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@R/dataProcess.R` around lines 240 - 253, Register an on.exit cleanup
immediately after creating the cluster used by the summarized_results parallel
branches, ensuring parallel::stopCluster(cl) runs even when either
parallel::parLapply call fails. Preserve the existing successful execution and
outer tryCatch behavior.

Comment thread R/dataProcess.R
Comment on lines +246 to 252
summarized_results = parallel::parLapply(cl, protein_data, function(single_protein) {
MSstatsSummarizeSingleLinear(
single_protein,
impute,
censored_symbol,
impute,
censored_symbol,
remove50missing,
aft_iterations)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Forward equal_variance to the linear summarizer.

equal_variance is exported but never passed at Line 247, so equalFeatureVar = FALSE silently uses MSstatsSummarizeSingleLinear()’s default (TRUE). The same omission exists in the single-core call at R/dataProcess.R Lines 321-323; update both paths and add non-default parity coverage.

Proposed fix
 MSstatsSummarizeSingleLinear(
     single_protein,
     impute,
     censored_symbol,
     remove50missing,
-    aft_iterations)
+    aft_iterations,
+    equal_variances = equal_variance)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
summarized_results = parallel::parLapply(cl, protein_data, function(single_protein) {
MSstatsSummarizeSingleLinear(
single_protein,
impute,
censored_symbol,
impute,
censored_symbol,
remove50missing,
aft_iterations)
summarized_results = parallel::parLapply(cl, protein_data, function(single_protein) {
MSstatsSummarizeSingleLinear(
single_protein,
impute,
censored_symbol,
remove50missing,
aft_iterations,
equal_variances = equal_variance)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@R/dataProcess.R` around lines 246 - 252, Update both the parallel
`MSstatsSummarizeSingleLinear` call inside `parallel::parLapply` and the
single-core call in the same data-processing flow to forward the exported
`equal_variance` value as the summarizer’s `equalFeatureVar` argument. Add
coverage exercising `equalFeatureVar = FALSE` and confirming matching behavior
between parallel and single-core paths.

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