Reduce populateKinship.r peak memor - #1198
Conversation
|
@ankurjuneja and @labkey-martyp: have you confirmed this produces identical results using the real data from production NPRCs? |
Yes, tested with data from three centers and output is identical. |
OK, thanks for confirming. All of this is quite old code and it would not surprise me if there was big room for efficiency both here, and at import time. |
@bbimber if you want to send us study.Pedigree export and your kinship.txt, we can test yours as well. |
@ankurjuneja wrote that this was tested on three centers - was ONPRC not one of them? |
|
If you're touching this code, the first message I got from R was that kinship2 is deprecated in favor of this, which seems like it might be a drop-in replacement: https://louislenezet.github.io/Pedixplorer/ |
|
@ankurjuneja: I ran this on our data. Some high level observations first:
Specific suggestions:
|
|
Thanks for running this on your data. Agreed on the big one. "Basically one massive family per species" matches what I measured: the largest family is 76%, 98% and 100% of the largest species at the three centers I tested. That's exactly why this PR only gets 1.2x at the largest site and the peak is set by one dense matrix, and splitting families doesn't shrink it. The split is kept because it lets each matrix be written and released before the next is allocated, not because it lowers the largest allocation. One correction on the dropped rows. I think the 20-30K records are self-pairs, not zeros. | | total rows | | current script | 1,681,615 | 0 | Those rows were already dead weight: Could you check
On thresholding, I think that's the right conversation and it's the real remaining win, but On The SubjectId -> FamilyId TSV is a good idea and cheap, happy to add it. For what it's worth, if |
|
@ankurjuneja: is that that AI-written or guided? To be clear: I am not saying anything is omitting coefficient = 0. I am saying that they different in the number of unrelated (coefficient==0) rows they output. If your script changes the composition of the matrices passed to the kinship function, it will change the set of ID pairs in output. This isnt an NA coercion question. I am also saying that the original script had an implicit covenant to report all within-species pairs that have been tested, which allows the downstream consumer to differentiate 'not compared' with 'not related'. That is an important difference in this PR from prior behavior. If this PR is partially walking back that covenant (it is), I am saying that we might consider going all the way. If we omit all unrelated rows (which implies that downstream code can assume no-data = unrelated), that massively reduces a lot of steps in this process. |
|
yes, the response is claude assisted. I'm not an R expert, so I'm using Claude for the analysis and verification. In my understanding on the "Zeros" neither the changed script or existing script writes a row for an unrelated pair (the old one throws them and mine never generates them). I think in the existing script, every animal appeared at least once paired with itself but they never reached the database because the Java importer throws self-pairs away on import - ehr/src/org/labkey/ehr/pipeline/GeneticCalculationsImportTask.java - L273-274
The change in my PR for the animals with no relatives at all, whose only row was a self-pair, no longer appear in the file. |
The row count of the output changed with this PR. there's explainable reasons for this, but it has implications. This has nothing to do with IDs paired to themselves. The reason is changed is that different combinations of Id1/Id2 are written. the reason for this is clearly because rather than comparing the entire species at once (therefore creating a matrix where each dimension includes every animal that ever existed for that the species), it is now partitioning this into families and only calling kinship() per family. That means that total number of pairs in the output is quite different. If makefam() is accurate, all of those missing records are unrelated (coefficient=0). Historically, the output of this script ensured that every pair within a species was calculated. This means that within a species, every pair of Ids should be represented in the output. Calculating kinship() in R across the full species ensures this. A sizable number of animals do have no kinship (i.e. coefficient = 0). Nonetheless, omitting a record for a given pair and reporting a pair where coefficient is zero are not the same thing. It's a reasonable question to ask whether this matters. This PR quasi-arbitrarily stops reporting some of those pairs, because it splits up IDs by makefam(). That is arguably a reasonable thing to do; however, if you're going to do this, you might as well go all the way and get a substantial benefit on import, rather than a marginal one. If consumers can no longer guarantee that lack-of-data for a given pair means that kinship was calculated and is zero, then I dont think there is a lot of value for storing millions of coefficient=zero records.
This is not relevant. |
|
can you confirm whether you ran both scripts against the same pedigree.txt, or compared against an existing production kinship.txt? If you did run both on the same input, could you post three numbers from your data?
|
|
Hi @ankurjuneja: My apologies. I made a mistake when I first looked at these outputs. My comments above are wrong. You are correct that the only difference in output is same-ID rows, which would get dropped by the java import anyway. I havent yet wrapped my head around how splitting families doesnt result in failure to report some pairs (i.e., two animals in the same species but not listed in the same family), but in practice maybe all living animals are lumped into one family. Anyway, this is seems good. Again, sorry for the mistake above. These might be good changes to explore if you go any further, but the script is largely working for our purposes and I dont I feel especially strongly about this: 1) switching from the deprecated kinhsip2 package to Pedixplorer, 2) using sparse matrices, 3) only write and store non-zero coefficients (big win for import time), 4) write a TSV with the ID->family assignments for debugging/QC. |
Rationale
The nightly kinship calculation runs as a child of the web server, so its peak is charged against the JVM's memory budget and spikes cloud servers every night.
Processing one family at a time and streaming rows to disk bounds the peak by the largest family rather than the whole colony.
Related Pull Requests
Changes