From a26ae2b041c1492738b3bb559ada4745285e2177 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 20:00:18 +0000 Subject: [PATCH 01/22] Harden SAM input and genome insert compatibility --- .../scripts/testGenomeInsertHardening.sh | 127 ++++++++++++++++++ extras/tests/testReadChunkConfig.cpp | 36 ++++- source/GenomeInsertAnnotations.cpp | 52 ++++++- source/GenomeInsertAnnotations.h | 1 + source/GenomeInsertIdentity.cpp | 15 +++ source/GenomeInsertOverlay.cpp | 26 ++++ source/Genome_genomeLoad.cpp | 11 ++ source/Genome_writeGenomeIndex.cpp | 6 +- source/Parameters.cpp | 13 +- source/ReadAlignChunk_processChunks.cpp | 54 ++++++-- source/ReadChunkConfig.cpp | 35 ++++- source/ReadChunkConfig.h | 12 +- 12 files changed, 357 insertions(+), 31 deletions(-) diff --git a/extras/tests/scripts/testGenomeInsertHardening.sh b/extras/tests/scripts/testGenomeInsertHardening.sh index 7d3c2d2f..0f3bd1f1 100755 --- a/extras/tests/scripts/testGenomeInsertHardening.sh +++ b/extras/tests/scripts/testGenomeInsertHardening.sh @@ -224,6 +224,125 @@ expect_failure "malformed-gtf" "expected 9 tab-separated GTF fields" \ --genomeInsertOutMode Overlay --genomeInsertOutDir "${out_root}/malformed-output" \ --outFileNamePrefix "${out_root}/malformed_" +unsupported_type_base="${out_root}/unsupported-genome-type-base" +cp -a "${base_index}" "${unsupported_type_base}" +awk -F '\t' -v OFS='\t' ' + $1=="genomeType" {$2="SuperTranscriptome"} + {print} +' "${unsupported_type_base}/genomeParameters.txt" \ + > "${unsupported_type_base}/genomeParameters.txt.new" +mv "${unsupported_type_base}/genomeParameters.txt.new" \ + "${unsupported_type_base}/genomeParameters.txt" +expect_failure "unsupported-genome-type" "supports only untransformed Full genome indexes" \ + "${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ + --genomeDir "${unsupported_type_base}" --genomeFastaFiles "${insert_fasta}" \ + --genomeInsertOutMode Overlay --genomeInsertOutDir "${out_root}/unsupported-type-output" \ + --outFileNamePrefix "${out_root}/unsupported-type_" + +transformed_base="${out_root}/transformed-base" +cp -a "${base_index}" "${transformed_base}" +awk -F '\t' -v OFS='\t' ' + $1=="genomeTransformType" {$2="Diploid"} + {print} +' "${transformed_base}/genomeParameters.txt" \ + > "${transformed_base}/genomeParameters.txt.new" +mv "${transformed_base}/genomeParameters.txt.new" \ + "${transformed_base}/genomeParameters.txt" +expect_failure "unsupported-genome-transform" "supports only untransformed Full genome indexes" \ + "${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ + --genomeDir "${transformed_base}" --genomeFastaFiles "${insert_fasta}" \ + --genomeInsertOutMode Delta --genomeInsertOutDir "${out_root}/transformed-output" \ + --outFileNamePrefix "${out_root}/transformed_" + +malformed_count_base="${out_root}/malformed-count-base" +cp -a "${base_index}" "${malformed_count_base}" +sed '1s/.*/999/' "${malformed_count_base}/geneInfo.tab" \ + > "${malformed_count_base}/geneInfo.tab.new" +mv "${malformed_count_base}/geneInfo.tab.new" \ + "${malformed_count_base}/geneInfo.tab" +expect_failure "malformed-sidecar-count" "annotation sidecar count mismatch" \ + "${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ + --genomeDir "${malformed_count_base}" --genomeFastaFiles "${insert_fasta}" \ + --sjdbGTFfile "${insert_gtf}" \ + --genomeInsertOutDir "${out_root}/malformed-count-output" \ + --outFileNamePrefix "${out_root}/malformed-count_" + +malformed_integer_base="${out_root}/malformed-integer-base" +cp -a "${base_index}" "${malformed_integer_base}" +sed '1s/.*/2junk/' "${malformed_integer_base}/geneInfo.tab" \ + > "${malformed_integer_base}/geneInfo.tab.new" +mv "${malformed_integer_base}/geneInfo.tab.new" \ + "${malformed_integer_base}/geneInfo.tab" +expect_failure "malformed-sidecar-integer" "could not parse integer token '2junk'" \ + "${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ + --genomeDir "${malformed_integer_base}" --genomeFastaFiles "${insert_fasta}" \ + --sjdbGTFfile "${insert_gtf}" \ + --genomeInsertOutDir "${out_root}/malformed-integer-output" \ + --outFileNamePrefix "${out_root}/malformed-integer_" + +extra_reference_base="${out_root}/extra-reference-base" +cp -a "${base_index}" "${extra_reference_base}" +printf '@SQ\tSN:decoy_reference\tLN:100\n' \ + > "${extra_reference_base}/extraReferences.txt" +extra_reference_full="${out_root}/extra-reference-full" +"${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ + --genomeDir "${extra_reference_base}" --genomeFastaFiles "${insert_fasta}" \ + --sjdbGTFfile "${insert_gtf}" \ + --genomeInsertOutDir "${extra_reference_full}" \ + --outFileNamePrefix "${out_root}/extra-reference-full_" \ + > "${out_root}/extra-reference-full.log" 2>&1 +cmp "${extra_reference_base}/extraReferences.txt" \ + "${extra_reference_full}/extraReferences.txt" + +extra_reference_delta="${out_root}/extra-reference-delta" +"${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ + --genomeDir "${extra_reference_base}" --genomeFastaFiles "${insert_fasta}" \ + --sjdbGTFfile "${insert_gtf}" \ + --genomeInsertOutMode Delta --genomeInsertOutDir "${extra_reference_delta}" \ + --outFileNamePrefix "${out_root}/extra-reference-delta_" \ + > "${out_root}/extra-reference-delta.log" 2>&1 +printf '@SQ\tSN:changed_decoy_reference\tLN:100\n' \ + > "${extra_reference_base}/extraReferences.txt" +align_expect_failure "changed-extra-reference" "${extra_reference_delta}" \ + "genome insert artifact does not match the loaded base genome index" + +sam_input="${out_root}/reads.sam" +cat > "${sam_input}" <<'EOF_SAM_INPUT' +@HD VN:1.6 SO:unsorted +samRead 4 * 0 0 * * 0 0 ACGTACGTAC HHHHHHHHHH RG:Z:test +EOF_SAM_INPUT +"${star_bin}" \ + --runThreadN 96 \ + --genomeDir "${base_index}" \ + --readFilesIn "${sam_input}" \ + --readFilesType SAM SE \ + --outSAMtype None \ + --outSJtype None \ + --outFileNamePrefix "${out_root}/sam-high-thread_" \ + > "${out_root}/sam-high-thread.log" 2>&1 +if ! grep -Fq "Read input chunk buffer: 30000000 bytes total" \ + "${out_root}/sam-high-thread_Log.out"; then + echo "ERROR: SAM input unexpectedly enabled adaptive chunk sizing" >&2 + exit 1 +fi + +oversized_sam_input="${out_root}/oversized-attributes.sam" +{ + printf '@HD\tVN:1.6\tSO:unsorted\n' + printf 'samRead\t4\t*\t0\t0\t*\t*\t0\t0\tACGTACGTAC\tHHHHHHHHHH\tZZ:Z:' + head -c 10001 /dev/zero | tr '\0' A + printf '\n' +} > "${oversized_sam_input}" +expect_failure "oversized-sam-attributes" "SAM optional attributes exceed STAR's supported record limit" \ + "${star_bin}" \ + --runThreadN 96 \ + --genomeDir "${base_index}" \ + --readFilesIn "${oversized_sam_input}" \ + --readFilesType SAM SE \ + --outSAMtype None \ + --outSJtype None \ + --outFileNamePrefix "${out_root}/oversized-sam_" + inherited_overhang_output="${out_root}/inherited-overhang-output" "${star_bin}" --runMode genomeInsert --runThreadN "${threads}" \ --genomeDir "${base_index}" --genomeFastaFiles "${insert_fasta}" \ @@ -517,6 +636,14 @@ reference_namespace_collisions pass annotation_namespace_collisions pass transcript_context_validation pass strict_gtf_shape pass +unsupported_genome_type_rejected pass +transformed_genome_rejected pass +annotation_sidecar_count_validation pass +annotation_sidecar_integer_validation pass +extra_references_preserved pass +extra_references_identity_pinned pass +high_thread_sam_input pass +oversized_sam_attributes_rejected pass base_sjdb_overhang_contract pass nonempty_destination_preserved pass failed_stage_cleanup pass diff --git a/extras/tests/testReadChunkConfig.cpp b/extras/tests/testReadChunkConfig.cpp index 2e7e0722..7093a07d 100644 --- a/extras/tests/testReadChunkConfig.cpp +++ b/extras/tests/testReadChunkConfig.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include namespace { void expectInvalid(const std::function &operation) @@ -50,12 +52,35 @@ int main() assert(!explicitTarget.adaptive); assert(explicitTarget.effectiveTotalBytes == 4000000); + const ReadChunkConfig samInput = calculateReadChunkConfig( + 30000000, 0, 2, 96, reservePerEnd, false + ); + assert(!samInput.adaptive); + assert(samInput.effectiveTotalBytes == 30000000); + const ReadChunkConfig longRead = calculateReadChunkConfig( - 30000000, 0, 2, 96, 1100000 + 30000000, 0, 2, 96, 1100000, true, 8 ); assert(longRead.adaptive); - assert(longRead.effectiveTotalBytes == 2200002); - assert(longRead.perEndPayloadBytes == 1); + assert(longRead.effectiveTotalBytes == 17600016); + assert(longRead.perEndPayloadBytes == 7700008); + + std::vector boundedBuffer(17, '#'); + std::uint64_t used = 2; + assert(appendReadChunkRecord( + boundedBuffer.data(), 16, used, std::string("record") + )); + assert(used == 8); + assert(std::string(boundedBuffer.data()+2, 6) == "record"); + assert(boundedBuffer.at(16) == '#'); + + const std::vector beforeRejectedAppend = boundedBuffer; + const std::uint64_t usedBeforeRejectedAppend = used; + assert(!appendReadChunkRecord( + boundedBuffer.data(), 16, used, std::string(9, 'x') + )); + assert(used == usedBeforeRejectedAppend); + assert(boundedBuffer == beforeRejectedAppend); expectInvalid([&]() { calculateReadChunkConfig(30000000, 31000000, 2, 96, reservePerEnd); @@ -69,6 +94,11 @@ int main() expectInvalid([&]() { calculateReadChunkConfig(30000000, 0, 0, 96, reservePerEnd); }); + expectInvalid([&]() { + calculateReadChunkConfig( + 30000000, 0, 2, 96, reservePerEnd, true, 0 + ); + }); expectInvalid([&]() { calculateReadChunkConfig(8589934592ULL, 0, 2, 1, reservePerEnd); }); diff --git a/source/GenomeInsertAnnotations.cpp b/source/GenomeInsertAnnotations.cpp index 172a4d3c..9ab7b7ef 100644 --- a/source/GenomeInsertAnnotations.cpp +++ b/source/GenomeInsertAnnotations.cpp @@ -61,9 +61,20 @@ void copyPreferredIfExists(const string &dirPreferred, const string &dirFallback uint64 parseUint64Token(const string &token, const string &path, Parameters &P) { uint64 value=0; - istringstream tokenStream(token); - tokenStream >> value; - if (tokenStream.fail()) { + bool valid=!token.empty(); + for (string::const_iterator it=token.begin(); valid && it!=token.end(); ++it) { + if (*it<'0' || *it>'9') { + valid=false; + break; + }; + const uint64 digit=static_cast(*it-'0'); + if (value>(numeric_limits::max()-digit)/10) { + valid=false; + break; + }; + value=value*10+digit; + }; + if (!valid) { ostringstream errOut; errOut << "EXITING because of fatal ERROR: could not parse integer token '" << token << "' while writing " << path << "\n"; exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); @@ -71,6 +82,16 @@ uint64 parseUint64Token(const string &token, const string &path, Parameters &P) return value; } +uint64 checkedAdd(const uint64 value, const uint64 offset, const string &path, Parameters &P) +{ + if (value>numeric_limits::max()-offset) { + ostringstream errOut; + errOut << "EXITING because of fatal ERROR: integer overflow while writing " << path << "\n"; + exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); + }; + return value+offset; +} + vector splitFields(const string &line) { vector fields; @@ -102,7 +123,9 @@ void offsetField(vector &fields, const uint64 fieldIndex, const uint64 o errOut << "EXITING because of fatal ERROR: malformed annotation sidecar line while writing " << path << "\n"; exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); }; - fields.at(fieldIndex)=to_string(parseUint64Token(fields.at(fieldIndex), path, P)+offset); + fields.at(fieldIndex)=to_string( + checkedAdd(parseUint64Token(fields.at(fieldIndex), path, P), offset, path, P) + ); } bool readCountedFile(const string &path, uint64 &count, vector &lines, Parameters &P) @@ -122,13 +145,21 @@ bool readCountedFile(const string &path, uint64 &count, vector &lines, P string line; if (!getline(fileIn, line)) { - return true; + ostringstream errOut; + errOut << "EXITING because of fatal ERROR: missing count in annotation sidecar " << path << "\n"; + exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); }; count=parseUint64Token(line, path, P); while (getline(fileIn, line)) { lines.push_back(line); }; + if (count!=lines.size()) { + ostringstream errOut; + errOut << "EXITING because of fatal ERROR: annotation sidecar count mismatch in " << path << "\n"; + errOut << "Declared entries: " << count << "; observed entries: " << lines.size() << "\n"; + exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); + }; return true; } @@ -172,7 +203,7 @@ void mergeCountedFile(const string &dirBase, const string &dirInsert, const stri }; ofstream &fileOut=ofstrOpen(pathOut, ERROR_OUT, P); - fileOut << countBase+countInsert << "\n"; + fileOut << checkedAdd(countBase, countInsert, pathOut, P) << "\n"; for (auto &line : linesBase) { fileOut << line << "\n"; }; @@ -255,7 +286,9 @@ string offsetSjdbListFromGTFLine(const string &line, const uint64 geneOffset, co if (genesOut!="") { genesOut += ","; }; - genesOut += to_string(parseUint64Token(geneToken, path, P)+geneOffset); + genesOut += to_string( + checkedAdd(parseUint64Token(geneToken, path, P), geneOffset, path, P) + ); if (geneEnd==string::npos) { break; }; @@ -274,6 +307,11 @@ void genomeInsertCopyAnnotationSidecars(const string &dirIn, const string &dirOu }; } +void genomeInsertCopyReferenceSidecars(const string &dirIn, const string &dirOut) +{ + copyIfExists(dirIn, dirOut, "extraReferences.txt"); +} + void genomeInsertMergeAnnotationSidecars(const string &dirBase, const string &dirInsert, const string &dirOut, Parameters &P, bool copySjdbFiles) { if (copySjdbFiles) { diff --git a/source/GenomeInsertAnnotations.h b/source/GenomeInsertAnnotations.h index a6caef1a..5691e6aa 100644 --- a/source/GenomeInsertAnnotations.h +++ b/source/GenomeInsertAnnotations.h @@ -6,6 +6,7 @@ class Parameters; void genomeInsertCopyAnnotationSidecars(const string &dirIn, const string &dirOut); +void genomeInsertCopyReferenceSidecars(const string &dirIn, const string &dirOut); void genomeInsertMergeAnnotationSidecars(const string &dirBase, const string &dirInsert, const string &dirOut, Parameters &P, bool copySjdbFiles); #endif diff --git a/source/GenomeInsertIdentity.cpp b/source/GenomeInsertIdentity.cpp index c8517100..ff009a98 100644 --- a/source/GenomeInsertIdentity.cpp +++ b/source/GenomeInsertIdentity.cpp @@ -34,6 +34,8 @@ const vector optionalIdentityFiles = { "transcriptInfo.tab" }; +const string conditionalSemanticIdentityFile="extraReferences.txt"; + struct IdentityFile { string name; bool present; @@ -136,6 +138,10 @@ string genomeInsertBaseIdentityFromDirectory(const string &directory, uint threa for (vector::const_iterator it=optionalIdentityFiles.begin(); it!=optionalIdentityFiles.end(); ++it) { identities.push_back(identityFromPath(directory, *it, false, threadN)); } + uint64 conditionalSize=0; + if (regularFile(joinPath(directory, conditionalSemanticIdentityFile), &conditionalSize)) { + identities.push_back(identityFromPath(directory, conditionalSemanticIdentityFile, true, threadN)); + } return aggregateIdentity(identities); } catch (const exception &error) { identityFailure(error.what(), P); @@ -169,6 +175,15 @@ string genomeInsertBaseIdentityFromLoaded(Genome &genome) for (vector::const_iterator it=optionalIdentityFiles.begin(); it!=optionalIdentityFiles.end(); ++it) { identities.push_back(identityFromPath(genome.pGe.gDir, *it, false, P.runThreadN)); } + uint64 conditionalSize=0; + if (regularFile(joinPath(genome.pGe.gDir, conditionalSemanticIdentityFile), &conditionalSize)) { + identities.push_back(identityFromPath( + genome.pGe.gDir, + conditionalSemanticIdentityFile, + true, + P.runThreadN + )); + } return aggregateIdentity(identities); } catch (const exception &error) { identityFailure(error.what(), P); diff --git a/source/GenomeInsertOverlay.cpp b/source/GenomeInsertOverlay.cpp index af928841..554452ba 100644 --- a/source/GenomeInsertOverlay.cpp +++ b/source/GenomeInsertOverlay.cpp @@ -53,6 +53,29 @@ void fail(const string &message, int exitCode, Parameters &P) exitWithError(message, std::cerr, P.inOut->logMain, exitCode, P); } +void validateBaseGenomeKind(const string &baseDirectory, Parameters &P) +{ + const string parametersPath=baseDirectory+"/genomeParameters.txt"; + ifstream parametersFile(parametersPath.c_str()); + if (!parametersFile.good()) { + fail("EXITING because of fatal INPUT FILE error: could not open base genome parameters " + +parametersPath+"\n", EXIT_CODE_GENOME_FILES, P); + } + + Parameters baseParameters; + baseParameters.inOut=P.inOut; + baseParameters.scanAllLines(parametersFile, 3, -1); + if (baseParameters.pGe.gTypeString!="Full" || + baseParameters.pGe.transform.typeString!="None") { + ostringstream error; + error << "EXITING because --runMode genomeInsert supports only untransformed Full genome indexes\n"; + error << "Loaded genomeType=" << baseParameters.pGe.gTypeString + << "; genomeTransformType=" << baseParameters.pGe.transform.typeString << "\n"; + error << "SOLUTION: add sequences to the original untransformed Full index, then regenerate any transformed or transcriptome derivative.\n"; + fail(error.str(), EXIT_CODE_GENOME_FILES, P); + } +} + string stripTrailingSlash(string path) { while (path.size()>1 && path.back()=='/') path.erase(path.end()-1); @@ -852,6 +875,9 @@ string genomeInsertDeltaFilePath(const string &directory) void genomeInsertOutputPrepare(Parameters &P) { const string baseDirectory=addTrailingSlash(absoluteExistingPath(stripTrailingSlash(P.pGe.gDir), "base genomeDir", P), P); + // Overlay publication bypasses Genome::genomeLoad, so validate every + // genomeInsert output mode before staging any files. + validateBaseGenomeKind(stripTrailingSlash(baseDirectory), P); vector fastaFiles; for (vector::const_iterator it=P.pGe.gFastaFiles.begin(); it!=P.pGe.gFastaFiles.end(); ++it) { fastaFiles.push_back(absoluteExistingPath(*it, "inserted FASTA", P)); diff --git a/source/Genome_genomeLoad.cpp b/source/Genome_genomeLoad.cpp index f8146ea1..3e8ffe98 100755 --- a/source/Genome_genomeLoad.cpp +++ b/source/Genome_genomeLoad.cpp @@ -84,6 +84,17 @@ void Genome::genomeLoad(){//allocate and load Genome exitWithError(errOut.str(),std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); }; + if (P.runMode=="genomeInsert" && + (P1.pGe.gTypeString!="Full" || + P1.pGe.transform.typeString!="None")) { + ostringstream errOut; + errOut << "EXITING because --runMode genomeInsert supports only untransformed Full genome indexes\n"; + errOut << "Loaded genomeType=" << P1.pGe.gTypeString + << "; genomeTransformType=" << P1.pGe.transform.typeString << "\n"; + errOut << "SOLUTION: add sequences to the original untransformed Full index, then regenerate any transformed or transcriptome derivative.\n"; + exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_GENOME_FILES, P); + }; + //find chr starts from files chrInfoLoad(); diff --git a/source/Genome_writeGenomeIndex.cpp b/source/Genome_writeGenomeIndex.cpp index 90d85ebe..ca133cab 100644 --- a/source/Genome_writeGenomeIndex.cpp +++ b/source/Genome_writeGenomeIndex.cpp @@ -9,10 +9,10 @@ void writeAnnotationSidecars(Genome &genome, const string &dirOut) { if (!(genome.P.runMode=="genomeInsert" && genome.P.sjdbInsert.pass1 && genome.pGe.sjdbGTFfile!="-")) { genomeInsertCopyAnnotationSidecars(genome.pGe.gDir, dirOut); - return; + } else { + genomeInsertMergeAnnotationSidecars(genome.pGe.gDir, genome.P.sjdbInsert.outDir, dirOut, genome.P, true); }; - - genomeInsertMergeAnnotationSidecars(genome.pGe.gDir, genome.P.sjdbInsert.outDir, dirOut, genome.P, true); + genomeInsertCopyReferenceSidecars(genome.pGe.gDir, dirOut); } } diff --git a/source/Parameters.cpp b/source/Parameters.cpp index fa7753f6..94688ad0 100755 --- a/source/Parameters.cpp +++ b/source/Parameters.cpp @@ -1232,7 +1232,14 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters , std::cerr, inOut->logMain, EXIT_CODE_PARAMETER, *this); const uint64 chunkInReservePerEnd = - 2ULL*(DEF_readSeqLengthMax+1) + 2ULL*DEF_readNameLengthMax; + 2ULL*(DEF_readSeqLengthMax+1) + + 2ULL*DEF_readNameLengthMax + + (readFilesTypeN==10 ? BAM_ATTR_MaxSize : 0); +#ifdef COMPILE_FOR_LONG_READS + const uint32 chunkInMinimumRecordSlots=8; +#else + const uint32 chunkInMinimumRecordSlots=1; +#endif ReadChunkConfig readChunkConfig; try { readChunkConfig = calculateReadChunkConfig( @@ -1240,7 +1247,9 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters readChunkSizeBytes, readNends, runThreadN, - chunkInReservePerEnd + chunkInReservePerEnd, + readFilesTypeN!=10, + chunkInMinimumRecordSlots ); } catch (const std::invalid_argument &error) { exitWithError( diff --git a/source/ReadAlignChunk_processChunks.cpp b/source/ReadAlignChunk_processChunks.cpp index 611613ec..3ed5d06d 100755 --- a/source/ReadAlignChunk_processChunks.cpp +++ b/source/ReadAlignChunk_processChunks.cpp @@ -3,6 +3,7 @@ #include "ErrorWarning.h" #include "SequenceFuns.h" #include "GlobalVariables.h" +#include "ReadChunkConfig.h" inline uint64 fastqReadOneLine(ifstream &streamIn, char *arrIn); inline void removeStringEndControl(string &str); @@ -80,16 +81,6 @@ void ReadAlignChunk::processChunks() {//read-map-write chunks imate1=0; }; - //read ID or number - if (P.outSAMreadID=="Number") { - chunkInSizeBytesTotal[imate1] += sprintf(chunkIn[imate1] + chunkInSizeBytesTotal[imate1], "@%llu", P.iReadAll); - } else { - chunkInSizeBytesTotal[imate1] += sprintf(chunkIn[imate1] + chunkInSizeBytesTotal[imate1], "@%s", str1.c_str()); - }; - - //iReadAll, passFilterIllumina, passFilterIllumina - chunkInSizeBytesTotal[imate1] += sprintf(chunkIn[imate1] + chunkInSizeBytesTotal[imate1], " %llu %c %i", P.iReadAll, passFilterIllumina, P.readFilesIndex); - string dummy; for (int ii=3; ii<=9; ii++) P.inOut->readIn[0] >> dummy; //skip fields until sequence @@ -100,10 +91,49 @@ void ReadAlignChunk::processChunks() {//read-map-write chunks revComplementNucleotides(seq1); reverse(qual1.begin(),qual1.end()); }; - + string attrs; getline(P.inOut->readIn[0], attrs); //rest of the SAM line: str1 is now all SAM attributes - it's added to the read ID line (1st "fastq" line) - chunkInSizeBytesTotal[imate1] += sprintf(chunkIn[imate1] + chunkInSizeBytesTotal[imate1], "%s\n%s\n+\n%s\n", attrs.c_str(), seq1.c_str(), qual1.c_str()); + if (attrs.size()>BAM_ATTR_MaxSize) { + ostringstream errOut; + errOut << ERROR_OUT << " EXITING because SAM optional attributes exceed STAR's supported record limit\n"; + errOut << "Attribute bytes: " << attrs.size() + << "; supported maximum: " << BAM_ATTR_MaxSize << "\n"; + errOut << "SOLUTION: remove unnecessary SAM tags or select a smaller tag set before mapping.\n"; + exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_INPUT_FILES, P); + }; + string samRecord; + samRecord.reserve(str1.size()+attrs.size()+seq1.size()+qual1.size()+64); + samRecord += '@'; + samRecord += P.outSAMreadID=="Number" ? to_string(P.iReadAll) : str1; + samRecord += ' '; + samRecord += to_string(P.iReadAll); + samRecord += ' '; + samRecord += passFilterIllumina; + samRecord += ' '; + samRecord += to_string(P.readFilesIndex); + samRecord += attrs; + samRecord += '\n'; + samRecord += seq1; + samRecord += "\n+\n"; + samRecord += qual1; + samRecord += '\n'; + + std::uint64_t samChunkUsed=chunkInSizeBytesTotal[imate1]; + if (!appendReadChunkRecord( + chunkIn[imate1], + P.chunkInSizeBytesArray, + samChunkUsed, + samRecord + )) { + ostringstream errOut; + errOut << ERROR_OUT << " EXITING because a SAM input record exceeds the configured read input buffer\n"; + errOut << "Record bytes: " << samRecord.size() + << "; available per-end buffer bytes: " << P.chunkInSizeBytesArray << "\n"; + errOut << "SOLUTION: increase the first value of --limitIObufferSize or --readChunkSizeBytes.\n"; + exitWithError(errOut.str(), std::cerr, P.inOut->logMain, EXIT_CODE_INPUT_FILES, P); + }; + chunkInSizeBytesTotal[imate1]=samChunkUsed; }; }; diff --git a/source/ReadChunkConfig.cpp b/source/ReadChunkConfig.cpp index 70cdb518..ab1018c3 100644 --- a/source/ReadChunkConfig.cpp +++ b/source/ReadChunkConfig.cpp @@ -1,6 +1,7 @@ #include "ReadChunkConfig.h" #include +#include #include #include #include @@ -22,12 +23,17 @@ ReadChunkConfig calculateReadChunkConfig( std::uint64_t requestedTotalBytes, std::uint32_t readEnds, std::uint32_t runThreads, - std::uint64_t reservePerEnd + std::uint64_t reservePerEnd, + bool adaptiveAllowed, + std::uint32_t minimumRecordSlots ) { if (readEnds == 0) { throw invalidValue("the number of read ends is zero"); } + if (minimumRecordSlots == 0) { + throw invalidValue("the minimum record slot count is zero"); + } if (reservePerEnd == std::numeric_limits::max()) { throw invalidValue("the per-end reserve overflows"); } @@ -37,6 +43,12 @@ ReadChunkConfig calculateReadChunkConfig( throw invalidValue("the minimum total buffer size overflows"); } const std::uint64_t minimumTotalBytes = minimumPerEnd * readEnds; + if (minimumTotalBytes > + std::numeric_limits::max() / minimumRecordSlots) { + throw invalidValue("the adaptive record floor overflows"); + } + const std::uint64_t adaptiveMinimumTotalBytes = + minimumTotalBytes * minimumRecordSlots; if (maximumTotalBytes < minimumTotalBytes) { throw invalidValue( "limitIObufferSize input bytes must be at least " + @@ -53,10 +65,10 @@ ReadChunkConfig calculateReadChunkConfig( bool adaptive = false; if (requestedTotalBytes > 0) { effectiveTotalBytes = requestedTotalBytes; - } else if (runThreads >= adaptiveThreadThreshold) { + } else if (adaptiveAllowed && runThreads >= adaptiveThreadThreshold) { const std::uint64_t autoTarget = std::max( adaptiveTargetBytes, - minimumTotalBytes + adaptiveMinimumTotalBytes ); effectiveTotalBytes = std::min(maximumTotalBytes, autoTarget); adaptive = effectiveTotalBytes < maximumTotalBytes; @@ -81,3 +93,20 @@ ReadChunkConfig calculateReadChunkConfig( adaptive }; } + +bool appendReadChunkRecord( + char *buffer, + std::uint64_t capacity, + std::uint64_t &used, + const std::string &record +) +{ + if (buffer == NULL || used > capacity || record.size() > capacity-used) { + return false; + } + if (!record.empty()) { + memcpy(buffer+used, record.data(), record.size()); + } + used += record.size(); + return true; +} diff --git a/source/ReadChunkConfig.h b/source/ReadChunkConfig.h index 1f266096..82f7a188 100644 --- a/source/ReadChunkConfig.h +++ b/source/ReadChunkConfig.h @@ -2,6 +2,7 @@ #define READ_CHUNK_CONFIG_H #include +#include struct ReadChunkConfig { std::uint64_t effectiveTotalBytes; @@ -15,7 +16,16 @@ ReadChunkConfig calculateReadChunkConfig( std::uint64_t requestedTotalBytes, std::uint32_t readEnds, std::uint32_t runThreads, - std::uint64_t reservePerEnd + std::uint64_t reservePerEnd, + bool adaptiveAllowed = true, + std::uint32_t minimumRecordSlots = 1 +); + +bool appendReadChunkRecord( + char *buffer, + std::uint64_t capacity, + std::uint64_t &used, + const std::string &record ); #endif From d4bd90411b29a2f5587ddd111e1756364c9d8017 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 20:07:05 +0000 Subject: [PATCH 02/22] Respect cgroup memory limits in index optimizations --- extras/tests/scripts/testSystemMemory.sh | 16 ++ extras/tests/testSystemMemory.cpp | 141 ++++++++++++ source/Genome_genomeGenerate.cpp | 31 +-- source/Makefile | 2 +- source/SystemMemory.cpp | 281 +++++++++++++++++++++++ source/SystemMemory.h | 24 ++ source/genomeSAindex.cpp | 30 +-- source/sjdbBuildIndex.cpp | 123 ++++++---- 8 files changed, 563 insertions(+), 85 deletions(-) create mode 100755 extras/tests/scripts/testSystemMemory.sh create mode 100644 extras/tests/testSystemMemory.cpp create mode 100644 source/SystemMemory.cpp create mode 100644 source/SystemMemory.h diff --git a/extras/tests/scripts/testSystemMemory.sh b/extras/tests/scripts/testSystemMemory.sh new file mode 100755 index 00000000..c664c9d3 --- /dev/null +++ b/extras/tests/scripts/testSystemMemory.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +build_dir="$(mktemp -d "${TMPDIR:-/tmp}/blackstar-system-memory-test.XXXXXX")" +trap 'rm -rf "${build_dir}"' EXIT + +"${CXX:-g++}" \ + -std=c++11 -Wall -Wextra -fsanitize=address,undefined \ + -I"${repo_root}/source" \ + "${repo_root}/extras/tests/testSystemMemory.cpp" \ + "${repo_root}/source/SystemMemory.cpp" \ + -o "${build_dir}/testSystemMemory" + +ASAN_OPTIONS=detect_leaks=0 \ + "${build_dir}/testSystemMemory" "${build_dir}/fixtures" diff --git a/extras/tests/testSystemMemory.cpp b/extras/tests/testSystemMemory.cpp new file mode 100644 index 00000000..63699fc4 --- /dev/null +++ b/extras/tests/testSystemMemory.cpp @@ -0,0 +1,141 @@ +#include "SystemMemory.h" + +#include +#include +#include +#include +#include +#include + +namespace { +void require(bool condition, const std::string &message) +{ + if (!condition) { + throw std::runtime_error(message); + } +} + +void makeDirectory(const std::string &path) +{ + if (mkdir(path.c_str(), 0700)!=0) { + throw std::runtime_error("could not create fixture directory "+path); + } +} + +void writeFile(const std::string &path, const std::string &contents) +{ + std::ofstream output(path.c_str()); + if (!output.good()) { + throw std::runtime_error("could not create fixture file "+path); + } + output << contents; +} + +void makeProcTree(const std::string &root) +{ + makeDirectory(root); + makeDirectory(root+"/proc"); + makeDirectory(root+"/proc/self"); + makeDirectory(root+"/cgroup"); +} + +SystemMemoryAvailability inspect(const std::string &root) +{ + return systemMemoryAvailability( + root+"/proc/meminfo", + root+"/proc/self/cgroup", + root+"/cgroup" + ); +} +} + +int main(int argc, char **argv) +{ + if (argc!=2) { + std::cerr << "usage: testSystemMemory ROOT\n"; + return 2; + } + + try { + const std::string base=argv[1]; + makeDirectory(base); + + const std::string hostOnly=base+"/host-only"; + makeProcTree(hostOnly); + writeFile(hostOnly+"/proc/meminfo", "MemAvailable: 1000 kB\n"); + writeFile(hostOnly+"/proc/self/cgroup", "0::/\n"); + writeFile(hostOnly+"/cgroup/memory.max", "max\n"); + writeFile(hostOnly+"/cgroup/memory.current", "500\n"); + SystemMemoryAvailability result=inspect(hostOnly); + require(result.hostAvailableKnown, "host MemAvailable was not detected"); + require(!result.cgroupAvailableKnown, "unlimited cgroup was treated as finite"); + require(result.effectiveAvailableBytes==1024000, "host-only bound is wrong"); + + const std::string unified=base+"/unified"; + makeProcTree(unified); + makeDirectory(unified+"/cgroup/team"); + makeDirectory(unified+"/cgroup/team/job"); + writeFile(unified+"/proc/meminfo", "MemAvailable: 10000 kB\n"); + writeFile(unified+"/proc/self/cgroup", "0::/team/job\n"); + writeFile(unified+"/cgroup/memory.max", "max\n"); + writeFile(unified+"/cgroup/memory.current", "1\n"); + writeFile(unified+"/cgroup/team/memory.max", "10000\n"); + writeFile(unified+"/cgroup/team/memory.current", "7000\n"); + writeFile(unified+"/cgroup/team/job/memory.max", "6000\n"); + writeFile(unified+"/cgroup/team/job/memory.current", "1000\n"); + result=inspect(unified); + require(result.cgroupAvailableKnown, "cgroup v2 bound was not detected"); + require(result.cgroupAvailableBytes==3000, "ancestor cgroup v2 bound is wrong"); + require(result.effectiveAvailableBytes==3000, "effective cgroup v2 bound is wrong"); + + const std::string exhausted=base+"/exhausted"; + makeProcTree(exhausted); + writeFile(exhausted+"/proc/meminfo", "MemAvailable: 10000 kB\n"); + writeFile(exhausted+"/proc/self/cgroup", "0::/\n"); + writeFile(exhausted+"/cgroup/memory.max", "1000\n"); + writeFile(exhausted+"/cgroup/memory.current", "1200\n"); + result=inspect(exhausted); + require(result.cgroupAvailableKnown, "exhausted cgroup was not detected"); + require(result.effectiveAvailableBytes==0, "exhausted cgroup did not clamp to zero"); + + const std::string unreadableUsage=base+"/unreadable-usage"; + makeProcTree(unreadableUsage); + writeFile(unreadableUsage+"/proc/meminfo", "MemAvailable: 10000 kB\n"); + writeFile(unreadableUsage+"/proc/self/cgroup", "0::/\n"); + writeFile(unreadableUsage+"/cgroup/memory.max", "1000\n"); + result=inspect(unreadableUsage); + require(result.cgroupAvailableKnown, "finite cgroup with missing usage was ignored"); + require(result.effectiveAvailableBytes==0, "missing cgroup usage did not fail closed"); + + const std::string legacy=base+"/legacy"; + makeProcTree(legacy); + makeDirectory(legacy+"/cgroup/memory"); + makeDirectory(legacy+"/cgroup/memory/team"); + makeDirectory(legacy+"/cgroup/memory/team/job"); + writeFile(legacy+"/proc/meminfo", "MemAvailable: 10000 kB\n"); + writeFile(legacy+"/proc/self/cgroup", "5:cpu,memory:/team/job\n"); + writeFile(legacy+"/cgroup/memory/memory.limit_in_bytes", "1152921504606846976\n"); + writeFile(legacy+"/cgroup/memory/memory.usage_in_bytes", "1\n"); + writeFile(legacy+"/cgroup/memory/team/memory.limit_in_bytes", "10000\n"); + writeFile(legacy+"/cgroup/memory/team/memory.usage_in_bytes", "6000\n"); + writeFile(legacy+"/cgroup/memory/team/job/memory.limit_in_bytes", "8000\n"); + writeFile(legacy+"/cgroup/memory/team/job/memory.usage_in_bytes", "2500\n"); + result=inspect(legacy); + require(result.cgroupAvailableKnown, "cgroup v1 bound was not detected"); + require(result.cgroupAvailableBytes==4000, "ancestor cgroup v1 bound is wrong"); + require(result.effectiveAvailableBytes==4000, "effective cgroup v1 bound is wrong"); + + const std::string malformed=base+"/malformed"; + makeProcTree(malformed); + writeFile(malformed+"/proc/meminfo", "MemAvailable: invalid kB\n"); + writeFile(malformed+"/proc/self/cgroup", "not-a-cgroup-record\n"); + result=inspect(malformed); + require(!result.effectiveAvailableKnown, "malformed sources produced a memory bound"); + } catch (const std::exception &error) { + std::cerr << "system memory tests failed: " << error.what() << "\n"; + return 1; + } + + std::cout << "system memory tests passed\n"; + return 0; +} diff --git a/source/Genome_genomeGenerate.cpp b/source/Genome_genomeGenerate.cpp index 263305aa..047dec77 100755 --- a/source/Genome_genomeGenerate.cpp +++ b/source/Genome_genomeGenerate.cpp @@ -18,6 +18,7 @@ #include "sjdbInsertJunctions.h" #include "genomeScanFastaFiles.h" #include "genomeSAindex.h" +#include "SystemMemory.h" #include "serviceFuns.cpp" #include "streamFuns.h" @@ -27,22 +28,6 @@ char* globalG; uint globalL; -static uint systemAvailableMemoryBytes() -{ -#ifdef __linux__ - ifstream memInfo("/proc/meminfo"); - string field, unit; - uint value=0; - - while (memInfo >> field >> value >> unit) { - if (field=="MemAvailable:") { - return value*1024LLU; - }; - }; -#endif - return 0; -}; - inline int funCompareSuffixesFromWord ( const void *a, const void *b, uint wordStart){ const char *ga=(globalG-7LLU)+(*((uint*)a)); @@ -392,9 +377,11 @@ void Genome::genomeGenerate() { const uint64 saRamPeakBytes=max(saAllChunkScatterBytes,saRamPackPeakBytes); const uint64 saRamHeadroomBytes=max(saRamPeakBytes/10,(uint64) 2000000000LLU); const uint64 saRamRequiredBytes=saRamPeakBytes+saRamHeadroomBytes; - const uint64 systemAvailableBytes=systemAvailableMemoryBytes(); + const SystemMemoryAvailability memoryAvailability=systemMemoryAvailability(); + const uint64 systemAvailableBytes=memoryAvailability.effectiveAvailableBytes; const bool saRamLimitOK=saAvailableBytes>=saRamRequiredBytes; - const bool saRamSystemOK=systemAvailableBytes>0 && systemAvailableBytes>=saRamRequiredBytes; + const bool saRamSystemOK=!memoryAvailability.effectiveAvailableKnown || + systemAvailableBytes>=saRamRequiredBytes; bool saChunksInMemoryActive=saRamLimitOK && saRamSystemOK; P.inOut->logMain << "SA chunk all-scatter available bytes: " << saAvailableBytes << "; required temporary bytes: " << saAllChunkScatterBytes << "\n" <logMain << "SA chunk estimated batched-fill batches: " << saChunkBatchN << "\n" <logMain << "SA chunk sort prefix length: " << indPrefLen << "\n" <logMain << "SA chunk sort granularity: " << (saChunkBatchFill ? "prefix-bin" : "chunk") << "\n" <logMain << "SA chunk retained bytes: " << saRetainedChunkBytes << "; RAM peak bytes: " << saRamPeakBytes << "; RAM headroom bytes: " << saRamHeadroomBytes << "\n" <logMain << "SA chunk system available bytes: " << systemAvailableBytes << "; limit available bytes: " << saAvailableBytes << "\n" <logMain << "SA chunk host available bytes: " + << (memoryAvailability.hostAvailableKnown ? to_string(memoryAvailability.hostAvailableBytes) : "unknown") + << "; cgroup available bytes: " + << (memoryAvailability.cgroupAvailableKnown ? to_string(memoryAvailability.cgroupAvailableBytes) : "unbounded-or-unknown") + << "; effective available bytes: " + << (memoryAvailability.effectiveAvailableKnown ? to_string(systemAvailableBytes) : "unknown") + << "; limit available bytes: " << saAvailableBytes << "\n" < +#include +#include +#include +#include +#include + +namespace { +bool parseUint64(const std::string &token, std::uint64_t &value) +{ + if (token.empty()) { + return false; + } + + value=0; + for (std::string::const_iterator it=token.begin(); it!=token.end(); ++it) { + if (*it<'0' || *it>'9') { + return false; + } + const std::uint64_t digit=static_cast(*it-'0'); + if (value>(std::numeric_limits::max()-digit)/10) { + return false; + } + value=value*10+digit; + } + return true; +} + +bool readHostAvailable(const std::string &path, std::uint64_t &bytes) +{ + std::ifstream input(path.c_str()); + std::string field; + std::string valueToken; + std::string unit; + while (input >> field >> valueToken >> unit) { + if (field!="MemAvailable:") { + continue; + } + + std::uint64_t kibibytes=0; + if (unit!="kB" || !parseUint64(valueToken, kibibytes) || + kibibytes>std::numeric_limits::max()/1024) { + return false; + } + bytes=kibibytes*1024; + return true; + } + return false; +} + +bool directoryExists(const std::string &path) +{ + struct stat status; + return stat(path.c_str(), &status)==0 && S_ISDIR(status.st_mode); +} + +bool readCgroupValue( + const std::string &path, + bool allowUnlimited, + std::uint64_t &value, + bool &unlimited +) +{ + std::ifstream input(path.c_str()); + std::string token; + if (!(input >> token)) { + return false; + } + if (allowUnlimited && token=="max") { + unlimited=true; + value=0; + return true; + } + unlimited=false; + return parseUint64(token, value); +} + +std::string joinCgroupPath( + const std::string &cgroupRoot, + const std::string &cgroupPath +) +{ + std::string relative=cgroupPath; + while (!relative.empty() && relative[0]=='/') { + relative.erase(relative.begin()); + } + return relative.empty() ? cgroupRoot : cgroupRoot+"/"+relative; +} + +std::string parentPath(const std::string &path, const std::string &root) +{ + if (path==root) { + return root; + } + const std::string::size_type slash=path.find_last_of('/'); + if (slash==std::string::npos || slash::max(); + std::string current=start; + + while (true) { + std::uint64_t limit=0; + std::uint64_t usage=0; + bool unlimited=false; + bool usageUnlimited=false; + const bool limitRead=readCgroupValue( + current+"/"+limitName, + unified, + limit, + unlimited + ); + const bool usageRead=readCgroupValue( + current+"/"+usageName, + false, + usage, + usageUnlimited + ); + const bool legacyUnlimited=!unified && limit>=(1ULL<<60); + if (limitRead && !unlimited && !legacyUnlimited) { + // A finite limit without a readable usage value is not safe to + // treat as available capacity for an optional allocation. + const std::uint64_t remaining=usageRead && usage +#include + +struct SystemMemoryAvailability { + std::uint64_t hostAvailableBytes; + std::uint64_t cgroupAvailableBytes; + std::uint64_t effectiveAvailableBytes; + bool hostAvailableKnown; + bool cgroupAvailableKnown; + bool effectiveAvailableKnown; +}; + +SystemMemoryAvailability systemMemoryAvailability(); + +SystemMemoryAvailability systemMemoryAvailability( + const std::string &memInfoPath, + const std::string &selfCgroupPath, + const std::string &cgroupRoot +); + +#endif diff --git a/source/genomeSAindex.cpp b/source/genomeSAindex.cpp index ce4be41f..89ea1847 100644 --- a/source/genomeSAindex.cpp +++ b/source/genomeSAindex.cpp @@ -2,8 +2,7 @@ #include "TimeFunctions.h" #include "SuffixArrayFuns.h" #include "ErrorWarning.h" - -#include +#include "SystemMemory.h" struct SAindexEvent { uint isa; @@ -11,20 +10,6 @@ struct SAindexEvent { int iL4; }; -static uint64 SAindexSystemAvailableMemoryBytes() -{ -#ifdef __linux__ - ifstream memInfo("/proc/meminfo"); - string field; - string unit; - uint64 value=0; - while (memInfo >> field >> value >> unit) { - if (field=="MemAvailable:") return value*1024LLU; - }; -#endif - return 0; -}; - static bool SAindexEventEqual(uint indFull1, int iL41, uint indFull2, int iL42) { return indFull1==indFull2 && iL41==iL42; @@ -215,8 +200,10 @@ void genomeSAindexChunk(char * G, PackedArray & SA, Parameters & P, PackedArray const uint64 ramHeadroomBytes=max(residentArrayBytes/20, 256000000LLU); const uint64 eventAvailableBytes=P.limitGenomeGenerateRAM>residentArrayBytes+ramHeadroomBytes ? P.limitGenomeGenerateRAM-residentArrayBytes-ramHeadroomBytes : 0; - const uint64 systemAvailableBytes=SAindexSystemAvailableMemoryBytes(); - const uint64 systemEventAvailableBytes=systemAvailableBytes>ramHeadroomBytes + const SystemMemoryAvailability memoryAvailability=systemMemoryAvailability(); + const uint64 systemAvailableBytes=memoryAvailability.effectiveAvailableBytes; + const uint64 systemEventAvailableBytes=memoryAvailability.effectiveAvailableKnown && + systemAvailableBytes>ramHeadroomBytes ? systemAvailableBytes-ramHeadroomBytes : 0; const uint64 eventBudgetBytes=min( min(eventAvailableBytes, systemEventAvailableBytes), 256000000LLU); @@ -227,7 +214,12 @@ void genomeSAindexChunk(char * G, PackedArray & SA, Parameters & P, PackedArray P.inOut->logMain << "SAindex resident-array estimate: " << residentArrayBytes << "; RAM headroom: " << ramHeadroomBytes - << "; system available: " << systemAvailableBytes + << "; host available: " + << (memoryAvailability.hostAvailableKnown ? to_string(memoryAvailability.hostAvailableBytes) : "unknown") + << "; cgroup available: " + << (memoryAvailability.cgroupAvailableKnown ? to_string(memoryAvailability.cgroupAvailableBytes) : "unbounded-or-unknown") + << "; effective available: " + << (memoryAvailability.effectiveAvailableKnown ? to_string(systemAvailableBytes) : "unknown") << "; event budget: " << eventBudgetBytes << "\n" << flush; uint* ind0=new uint[mapGen.pGe.gSAindexNbases]; diff --git a/source/sjdbBuildIndex.cpp b/source/sjdbBuildIndex.cpp index fa5d5b5a..3fc938fe 100644 --- a/source/sjdbBuildIndex.cpp +++ b/source/sjdbBuildIndex.cpp @@ -9,7 +9,9 @@ #include "streamFuns.h" #include "binarySearch2.h" #include "ErrorWarning.h" +#include "SystemMemory.h" #include +#include #include "funCompareUintAndSuffixes.h" @@ -41,66 +43,95 @@ static bool sjdbSortIndicesParallel(Parameters &P, uint64* indArray, uint nInd, return false; }; - const uint64 indBytes=2*nInd*sizeof(uint64); - const uint64 auxBytes=indBytes + (uint64) P.runThreadN*bucketN*2*sizeof(uint64) + (bucketN+1)*sizeof(uint64); + const uint64 indBytes=(uint64) 2*nInd*sizeof(uint64); + const uint64 counterBytes=(uint64) P.runThreadN*bucketN*2*sizeof(uint64); + const uint64 boundaryBytes=(bucketN+1)*sizeof(uint64); + const uint64 auxBytes=indBytes+counterBytes+boundaryBytes; if (P.limitGenomeGenerateRAM>0 && auxBytes>P.limitGenomeGenerateRAM/4) { qsort((void*) indArray, nInd, 2*sizeof(uint64), funCompareUintAndSuffixes); return false; }; - vector bucketCount((uint) P.runThreadN*bucketN,0); - #pragma omp parallel num_threads(P.runThreadN) - { - uint tid=(uint) omp_get_thread_num(); - uint64* threadBucketCount=bucketCount.data()+tid*bucketN; - #pragma omp for schedule(static) - for (uint ii=0; ii(auxBytes/10, 256000000LLU); + const uint64 systemBudgetBytes=memoryAvailability.effectiveAvailableKnown && + memoryAvailability.effectiveAvailableBytes>memoryHeadroomBytes + ? memoryAvailability.effectiveAvailableBytes-memoryHeadroomBytes : 0; + P.inOut->logMain << "Junction sort auxiliary bytes: " << auxBytes + << "; memory headroom: " << memoryHeadroomBytes + << "; effective available: " + << (memoryAvailability.effectiveAvailableKnown + ? to_string(memoryAvailability.effectiveAvailableBytes) + : "unknown") + << "\n" << flush; + if (memoryAvailability.effectiveAvailableKnown && auxBytes>systemBudgetBytes) { + P.inOut->logMain << "Junction sort fallback: serial qsort because the effective memory bound is too small\n" << flush; + qsort((void*) indArray, nInd, 2*sizeof(uint64), funCompareUintAndSuffixes); + return false; }; - vector bucketStart(bucketN+1,0); - for (uint iBucket=0; iBucket bucketCount((uint) P.runThreadN*bucketN,0); + #pragma omp parallel num_threads(P.runThreadN) + { + uint tid=(uint) omp_get_thread_num(); + uint64* threadBucketCount=bucketCount.data()+tid*bucketN; + #pragma omp for schedule(static) + for (uint ii=0; ii bucketThreadStart((uint) P.runThreadN*bucketN,0); - for (uint iBucket=0; iBucket bucketStart(bucketN+1,0); + for (uint iBucket=0; iBucket bucketThreadStart((uint) P.runThreadN*bucketN,0); + for (uint iBucket=0; iBucketlogMain << "Junction sort fallback: serial qsort after auxiliary allocation failure\n" << flush; + qsort((void*) indArray, nInd, 2*sizeof(uint64), funCompareUintAndSuffixes); + return false; + }; }; void sjdbBuildIndex (Parameters &P, char *Gsj, char *G, PackedArray &SA, PackedArray &SA2, PackedArray &SAi, Genome &mapGen, Genome &mapGen1) { From 0fc97302d097dc6841941206c4f5386697fc5ec9 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 20:10:38 +0000 Subject: [PATCH 03/22] Restore inherited NUMA policy after genome load --- extras/tests/testNumaMemoryPolicy.cpp | 13 ++++- source/NumaMemoryPolicy.cpp | 79 ++++++++++++++++++++++++++- source/NumaMemoryPolicy.h | 24 +++++++- source/STAR.cpp | 54 +++++++++++++++++- 4 files changed, 164 insertions(+), 6 deletions(-) diff --git a/extras/tests/testNumaMemoryPolicy.cpp b/extras/tests/testNumaMemoryPolicy.cpp index 3cde509b..c02b447f 100644 --- a/extras/tests/testNumaMemoryPolicy.cpp +++ b/extras/tests/testNumaMemoryPolicy.cpp @@ -107,14 +107,23 @@ int main() false, false, false, "invalid-request" ); + BlackstarNumaPolicyState state; const BlackstarNumaPolicyResult unchanged = blackstarApplyNumaMemoryPolicy( - "Default", "alignReads", "NoSharedMemory", 96 + "Default", "alignReads", "NoSharedMemory", 96, state ); passed &= !unchanged.active && unchanged.status == 0 && unchanged.effective == "Default" && - unchanged.reason == "explicit-default"; + unchanged.reason == "explicit-default" && + !state.restoreRequired; + + const BlackstarNumaPolicyRestoreResult restore = + blackstarRestoreNumaMemoryPolicy(state); + passed &= !restore.attempted && + restore.restored && + restore.status == 0 && + restore.reason == "not-required"; return passed ? 0 : 1; } diff --git a/source/NumaMemoryPolicy.cpp b/source/NumaMemoryPolicy.cpp index 3a5f493a..fde2c573 100644 --- a/source/NumaMemoryPolicy.cpp +++ b/source/NumaMemoryPolicy.cpp @@ -129,6 +129,34 @@ int applyInterleave(const std::vector &mask) }; return errno == 0 ? EIO : errno; } + +int restoreMemoryPolicy( + int mode, + const std::vector &mask, + unsigned long maximumNodes +) +{ + const int baseMode=basePolicyMode(mode); + const bool maskMustBeEmpty=baseMode==MPOL_DEFAULT +#ifdef MPOL_LOCAL + || baseMode==MPOL_LOCAL +#endif + ; + const bool maskEmpty=countNodes(mask)==0; + const unsigned long *maskPointer= + maskMustBeEmpty || maskEmpty ? NULL : mask.data(); + const unsigned long maxNodes= + maskPointer==NULL ? 0 : maximumNodes; + if (syscall( + SYS_set_mempolicy, + mode, + maskPointer, + maxNodes + ) == 0) { + return 0; + }; + return errno == 0 ? EIO : errno; +} #endif } @@ -192,9 +220,16 @@ BlackstarNumaPolicyResult blackstarApplyNumaMemoryPolicy( const std::string &requested, const std::string &runMode, const std::string &genomeLoad, - int runThreads + int runThreads, + BlackstarNumaPolicyState &state ) { + state.restoreRequired=false; + state.inheritedMode=0; + state.maximumNodes=0; + state.inheritedMask.clear(); + state.inheritedName="NotCaptured"; + BlackstarNumaPolicyResult result = { false, -1, @@ -218,7 +253,7 @@ BlackstarNumaPolicyResult blackstarApplyNumaMemoryPolicy( result.allowedNodeCount = allowedMemoryNodes(allowedMask); if (result.allowedNodeCount < 0) { result.status = errno == 0 ? EIO : errno; - } else if (requested == "Auto") { + } else { int inheritedMode = MPOL_DEFAULT; std::vector inheritedMask; const int inheritedStatus = @@ -227,6 +262,10 @@ BlackstarNumaPolicyResult blackstarApplyNumaMemoryPolicy( inheritedPolicy = classifyInheritedPolicy(inheritedMode); result.inherited = inheritedPolicyName(inheritedMode); result.inheritedNodeCount = countNodes(inheritedMask); + state.inheritedMode=inheritedMode; + state.maximumNodes=maximumNumaNodes; + state.inheritedMask=inheritedMask; + state.inheritedName=result.inherited; } else { result.status = inheritedStatus; result.inherited = "Unavailable"; @@ -274,6 +313,7 @@ BlackstarNumaPolicyResult blackstarApplyNumaMemoryPolicy( if (result.status == 0) { result.active = true; result.effective = "Interleave"; + state.restoreRequired=true; } else { result.reason = "set-mempolicy-failed"; }; @@ -282,3 +322,38 @@ BlackstarNumaPolicyResult blackstarApplyNumaMemoryPolicy( #endif return result; } + +BlackstarNumaPolicyRestoreResult blackstarRestoreNumaMemoryPolicy( + const BlackstarNumaPolicyState &state +) +{ + BlackstarNumaPolicyRestoreResult result = { + state.restoreRequired, + !state.restoreRequired, + 0, + state.restoreRequired ? "Interleave" : state.inheritedName, + state.restoreRequired ? "not-restored" : "not-required" + }; + if (!state.restoreRequired) { + return result; + } + +#if defined(__linux__) && defined(SYS_get_mempolicy) && defined(SYS_set_mempolicy) + result.status=restoreMemoryPolicy( + state.inheritedMode, + state.inheritedMask, + state.maximumNodes + ); + if (result.status==0) { + result.restored=true; + result.effective=state.inheritedName; + result.reason="restored"; + } else { + result.reason="restore-mempolicy-failed"; + } +#else + result.status=ENOTSUP; + result.reason="platform-unsupported"; +#endif + return result; +} diff --git a/source/NumaMemoryPolicy.h b/source/NumaMemoryPolicy.h index 415aa39f..c722be06 100644 --- a/source/NumaMemoryPolicy.h +++ b/source/NumaMemoryPolicy.h @@ -2,6 +2,7 @@ #define H_BLACKSTAR_NUMA_MEMORY_POLICY #include +#include enum BlackstarNumaInheritedPolicy { BlackstarNumaInheritedUnknown, @@ -28,6 +29,22 @@ struct BlackstarNumaPolicyResult { std::string reason; }; +struct BlackstarNumaPolicyState { + bool restoreRequired; + int inheritedMode; + unsigned long maximumNodes; + std::vector inheritedMask; + std::string inheritedName; +}; + +struct BlackstarNumaPolicyRestoreResult { + bool attempted; + bool restored; + int status; + std::string effective; + std::string reason; +}; + BlackstarNumaPolicyChoice blackstarSelectNumaMemoryPolicy( const std::string &requested, const std::string &runMode, @@ -42,7 +59,12 @@ BlackstarNumaPolicyResult blackstarApplyNumaMemoryPolicy( const std::string &requested, const std::string &runMode, const std::string &genomeLoad, - int runThreads + int runThreads, + BlackstarNumaPolicyState &state +); + +BlackstarNumaPolicyRestoreResult blackstarRestoreNumaMemoryPolicy( + const BlackstarNumaPolicyState &state ); #endif diff --git a/source/STAR.cpp b/source/STAR.cpp index 41ab0eef..da4f98a5 100755 --- a/source/STAR.cpp +++ b/source/STAR.cpp @@ -76,6 +76,8 @@ int main(int argInN, char *argIn[]) Parameters P; // all parameters P.inputParameters(argInN, argIn); + BlackstarNumaPolicyState numaPolicyState; + numaPolicyState.restoreRequired=false; if (P.runMode == "alignReads") { const BlackstarNumaPolicyResult numaPolicy = @@ -83,7 +85,8 @@ int main(int argInN, char *argIn[]) P.pGe.gLoadNumaPolicy, P.runMode, P.pGe.gLoad, - P.runThreadN + P.runThreadN, + numaPolicyState ); P.inOut->logMain << "BLACKSTAR_NUMA_POLICY" << "\trequested\t" << numaPolicy.requested @@ -215,6 +218,55 @@ int main(int argInN, char *argIn[]) Genome genomeMain(P, P.pGe); genomeMain.genomeLoad(); + if (numaPolicyState.restoreRequired) + { + int restoreFailureCount=0; + int restoreSuccessCount=0; + int restoreStatus=0; + string restoreReason="restored"; + string restoreEffective=numaPolicyState.inheritedName; + #pragma omp parallel num_threads(P.runThreadN) reduction(+:restoreFailureCount,restoreSuccessCount) + { + const BlackstarNumaPolicyRestoreResult restoreResult = + blackstarRestoreNumaMemoryPolicy(numaPolicyState); + if (restoreResult.restored) { + restoreSuccessCount++; + } else { + restoreFailureCount++; + #pragma omp critical + { + if (restoreStatus==0) { + restoreStatus=restoreResult.status; + restoreReason=restoreResult.reason; + restoreEffective=restoreResult.effective; + }; + }; + }; + }; + P.inOut->logMain << "BLACKSTAR_NUMA_RESTORE" + << "\tattempted_threads\t" << restoreSuccessCount+restoreFailureCount + << "\trestored_threads\t" << restoreSuccessCount + << "\teffective\t" << restoreEffective + << "\tstatus\t" << restoreStatus + << "\treason\t" << restoreReason + << '\n' << flush; + if (restoreFailureCount>0) { + ostringstream errOut; + errOut << "EXITING because BlackSTAR could not restore the inherited NUMA memory policy after genome loading\n"; + errOut << "Failed threads=" << restoreFailureCount + << "; status=" << restoreStatus + << "; reason=" << restoreReason << "\n"; + errOut << "SOLUTION: select --genomeLoadNumaPolicy Default or correct the host NUMA policy permissions.\n"; + exitWithError( + errOut.str(), + std::cerr, + P.inOut->logMain, + EXIT_CODE_RUNTIME, + P + ); + }; + }; + if (P.pGe.transform.outYes) { genomeMain.Var = new Variation(P, genomeMain.chrStart, genomeMain.chrNameIndex, false);//no variation for mapGen, only for genOut genomeMain.genomeOut.g->Var = new Variation(P, genomeMain.genomeOut.g->chrStart, genomeMain.genomeOut.g->chrNameIndex, P.var.yes); From 1d4ec4799ef1c0d4e89ce6a8cd0fa6ecad131735 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 20:15:33 +0000 Subject: [PATCH 04/22] Isolate and exercise STARlong builds --- .github/workflows/blackstar-ci.yml | 36 +++++++++++ extras/tests/scripts/testSTARlong.sh | 97 ++++++++++++++++++++++++++++ source/Makefile | 34 +++++++--- 3 files changed, 158 insertions(+), 9 deletions(-) create mode 100755 extras/tests/scripts/testSTARlong.sh diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index 608b57a7..2b97e4a8 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -99,7 +99,9 @@ jobs: - name: Run focused sanitizer tests run: | extras/tests/scripts/testAlignmentThreadAffinity.sh + extras/tests/scripts/testNumaMemoryPolicy.sh extras/tests/scripts/testReadChunkConfig.sh + extras/tests/scripts/testSystemMemory.sh extras/tests/scripts/testPackedArray.sh extras/tests/scripts/testSuffixComparator.sh extras/tests/scripts/testTranscriptInitialization.sh @@ -172,3 +174,37 @@ jobs: test "$(source/STAR --version)" = "2.7.11b-blackstar.3" source/STAR --version-json | jq -e '.blackstar_version == "1.0.0"' ldd source/STAR | grep -Eq 'libgomp|libomp' + + starlong-build-and-smoke: + runs-on: ubuntu-24.04 + timeout-minutes: 35 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y g++ make zlib1g-dev + + - name: Build short-read and long-read binaries in one tree + run: | + make -C source -j2 STAR + make -C source -j2 STARlong + test -x source/STAR + test -x source/STARlong + + - name: Exercise STARlong-only code and high-thread chunk sizing + env: + THREADS: "96" + KEEP_TEST_OUTPUT: "1" + OUT_DIR: ${{ runner.temp }}/starlong-smoke + run: extras/tests/scripts/testSTARlong.sh + + - name: Upload STARlong smoke evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blackstar-starlong-${{ github.run_id }} + if-no-files-found: ignore + retention-days: 14 + path: ${{ runner.temp }}/starlong-smoke diff --git a/extras/tests/scripts/testSTARlong.sh b/extras/tests/scripts/testSTARlong.sh new file mode 100755 index 00000000..db6ef317 --- /dev/null +++ b/extras/tests/scripts/testSTARlong.sh @@ -0,0 +1,97 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +star_long="${STARLONG_BIN:-${repo_root}/source/STARlong}" +threads="${THREADS:-96}" +out_root="${OUT_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/blackstar-starlong-smoke.XXXXXX")}" +keep_output="${KEEP_TEST_OUTPUT:-0}" + +if [[ ! -x "${star_long}" ]]; then + echo "ERROR: STARLONG_BIN is not executable: ${star_long}" >&2 + exit 1 +fi + +cleanup() { + if [[ "${keep_output}" == "1" ]]; then + echo "Keeping STARlong smoke output: ${out_root}" >&2 + else + rm -rf "${out_root}" + fi +} +trap cleanup EXIT + +mkdir -p "${out_root}/index" "${out_root}/alignment" +genome_fasta="${out_root}/genome.fa" +reads_fastq="${out_root}/reads.fq" +sequence_file="${out_root}/sequence.txt" + +awk ' + BEGIN { + state=17 + bases="ACGT" + for (ii=0; ii<120000; ii++) { + state=(state*48271)%2147483647 + printf "%s", substr(bases, state%4+1, 1) + } + printf "\n" + } +' > "${sequence_file}" +{ + printf '>chrLongSmoke\n' + fold -w 80 "${sequence_file}" +} > "${genome_fasta}" +long_read="$(cut -c 20001-22000 "${sequence_file}")" +{ + printf '@long_read_2000nt\n%s\n+\n' "${long_read}" + head -c 2000 /dev/zero | tr '\0' I + printf '\n' +} > "${reads_fastq}" + +"${star_long}" \ + --runMode genomeGenerate \ + --runThreadN 4 \ + --genomeDir "${out_root}/index" \ + --genomeFastaFiles "${genome_fasta}" \ + --genomeSAindexNbases 5 \ + --genomeChrBinNbits 10 \ + --limitGenomeGenerateRAM 200000000 \ + --outFileNamePrefix "${out_root}/build_" \ + > "${out_root}/build.log" 2>&1 + +"${star_long}" \ + --runThreadN "${threads}" \ + --genomeDir "${out_root}/index" \ + --readFilesIn "${reads_fastq}" \ + --outSAMtype None \ + --outSJtype None \ + --outFileNamePrefix "${out_root}/alignment/" \ + > "${out_root}/alignment.log" 2>&1 + +chunk_bytes="$( + awk ' + /Read input chunk buffer:/ { + print $5 + exit + } + ' "${out_root}/alignment/Log.out" +)" +if [[ -z "${chunk_bytes}" ]] || (( chunk_bytes < 8800000 )); then + echo "ERROR: STARlong adaptive chunk buffer did not retain eight long-read slots" >&2 + exit 1 +fi +if ! grep -Fq "mode=adaptive" "${out_root}/alignment/Log.out"; then + echo "ERROR: STARlong high-thread smoke did not use adaptive chunk sizing" >&2 + exit 1 +fi +if ! grep -Eq 'Number of input reads[[:space:]]*\|[[:space:]]*1$' \ + "${out_root}/alignment/Log.final.out"; then + echo "ERROR: STARlong did not process the 2000 nt smoke read" >&2 + exit 1 +fi + +printf 'check\tstatus\n' +printf 'clean_long_read_build\tpass\n' +printf 'long_read_2000nt_input\tpass\n' +printf 'high_thread_chunk_floor\tpass\n' diff --git a/source/Makefile b/source/Makefile index cf81a030..93812df9 100644 --- a/source/Makefile +++ b/source/Makefile @@ -98,6 +98,9 @@ OBJECTS = systemFunctions.o funPrimaryAlignMark.o \ bam_cat.o serviceFuns.o GlobalVariables.cpp \ BAMoutput.o BAMfunctions.o ReadAlign_alignBAM.o BAMbinSortByCoordinate.o signalFromBAM.o bamRemoveDuplicates.o BAMbinSortUnmapped.o +LONG_OBJECTS := $(patsubst %.o,%.long.o,$(filter %.o,$(OBJECTS))) +LONG_LINK_SOURCES := $(filter %.cpp %.c,$(OBJECTS)) + SOURCES := $(wildcard *.cpp) $(wildcard *.c) @@ -107,15 +110,25 @@ SOURCES := $(wildcard *.cpp) $(wildcard *.c) %.o : %.c $(CXX) -c $(CPPFLAGS) $(CFLAGS) $< +%.long.o : %.cpp + $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) -o $@ $< + +%.long.o : %.c + $(CXX) -c $(CPPFLAGS) $(CFLAGS) -D'COMPILE_FOR_LONG_READS' -o $@ $< + all: cleanCompileInfo STAR$(SFX) opal/opal.o : opal/opal.cpp opal/opal.h cd opal && \ $(CXX) -c -I./ -std=c++11 $(CPPFLAGS) $(CXXFLAGS) $(CXXFLAGSextra) $(CXXFLAGS_SIMD) opal.cpp +opal/opal.long.o : opal/opal.cpp opal/opal.h + cd opal && \ + $(CXX) -c -I./ -std=c++11 $(CPPFLAGS) $(CXXFLAGS) $(CXXFLAGSextra) $(CXXFLAGS_SIMD) -o opal.long.o opal.cpp + .PHONY: clean clean: - 'rm' -f *.o opal/opal.o STAR STARstatic STARlong Depend.list parametersDefault.xxd + 'rm' -f *.o *.long.o opal/opal.o opal/opal.long.o STAR STARstatic STARlong Depend.list Depend.long.list parametersDefault.xxd .PHONY: CLEAN CLEAN: clean @@ -140,7 +153,10 @@ Depend.list: $(SOURCES) parametersDefault.xxd htslib echo $(SOURCES) 'rm' -f ./Depend.list $(CXX) $(CXXFLAGS_common) -MM $^ >> Depend.list +Depend.long.list: Depend.list + sed -E 's/^([^[:space:]:]+)\.o:/\1.long.o:/' Depend.list > Depend.long.list include Depend.list +include Depend.long.list endif endif endif @@ -168,13 +184,13 @@ STARstatic$(SFX) : Depend.list parametersDefault.xxd $(OBJECTS) STARlong$(SFX) : CXXFLAGS := $(CXXFLAGSextra) $(CXXFLAGS_main) -D'COMPILE_FOR_LONG_READS' $(CXXFLAGS) STARlong$(SFX) : LDFLAGS := $(LDFLAGSextra) $(LDFLAGS_shared) $(LDFLAGS) -STARlong$(SFX) : Depend.list parametersDefault.xxd $(OBJECTS) - $(CXX) -o STARlong$(SFX) $(CXXFLAGS) $(OBJECTS) $(LDFLAGS) +STARlong$(SFX) : Depend.list Depend.long.list parametersDefault.xxd $(LONG_OBJECTS) $(LONG_LINK_SOURCES) + $(CXX) -o STARlong$(SFX) $(CXXFLAGS) $(LONG_OBJECTS) $(LONG_LINK_SOURCES) $(LDFLAGS) STARlongStatic$(SFX) : CXXFLAGS := $(CXXFLAGSextra) $(CXXFLAGS_main) -D'COMPILE_FOR_LONG_READS' $(CXXFLAGS) STARlongStatic$(SFX) : LDFLAGS := $(LDFLAGSextra) $(LDFLAGS_static) $(LDFLAGS) -STARlongStatic$(SFX) : Depend.list parametersDefault.xxd $(OBJECTS) - $(CXX) -o STARlong$(SFX) $(CXXFLAGS) $(OBJECTS) $(LDFLAGS) +STARlongStatic$(SFX) : Depend.list Depend.long.list parametersDefault.xxd $(LONG_OBJECTS) $(LONG_LINK_SOURCES) + $(CXX) -o STARlong$(SFX) $(CXXFLAGS) $(LONG_OBJECTS) $(LONG_LINK_SOURCES) $(LDFLAGS) @@ -190,8 +206,8 @@ gdb : Depend.list parametersDefault.xxd $(OBJECTS) gdb-long : CXXFLAGS := $(CXXFLAGSextra) $(CXXFLAGS_gdb) -D'COMPILE_FOR_LONG_READS' $(CXXFLAGS) gdb-long : LDFLAGS := $(LDFLAGSextra) $(LDFLAGS_gdb) $(LDFLAGS) -gdb-long : Depend.list parametersDefault.xxd $(OBJECTS) - $(CXX) -o STARlong $(CXXFLAGS) $(OBJECTS) $(LDFLAGS) +gdb-long : Depend.list Depend.long.list parametersDefault.xxd $(LONG_OBJECTS) $(LONG_LINK_SOURCES) + $(CXX) -o STARlong $(CXXFLAGS) $(LONG_OBJECTS) $(LONG_LINK_SOURCES) $(LDFLAGS) STARforMacStatic : CXXFLAGS := $(CXXFLAGSextra) $(CXXFLAGS_main) -D'COMPILE_FOR_MAC' $(CXXFLAGS) STARforMacStatic : LDFLAGS := $(LDFLAGSextra) $(LDFLAGS_Mac_static) $(LDFLAGS) @@ -200,5 +216,5 @@ STARforMacStatic : Depend.list parametersDefault.xxd $(OBJECTS) STARlongForMacStatic : CXXFLAGS := -D'COMPILE_FOR_LONG_READS' $(CXXFLAGSextra) $(CXXFLAGS_main) -D'COMPILE_FOR_MAC' $(CXXFLAGS) STARlongForMacStatic : LDFLAGS := $(LDFLAGSextra) $(LDFLAGS_Mac_static) $(LDFLAGS) -STARlongForMacStatic : Depend.list parametersDefault.xxd $(OBJECTS) - $(CXX) -o STARlong $(CXXFLAGS) $(OBJECTS) $(LDFLAGS) +STARlongForMacStatic : Depend.list Depend.long.list parametersDefault.xxd $(LONG_OBJECTS) $(LONG_LINK_SOURCES) + $(CXX) -o STARlong $(CXXFLAGS) $(LONG_OBJECTS) $(LONG_LINK_SOURCES) $(LDFLAGS) From 17264bb7593e274cd5dd5792cfee75c5e0338656 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 20:36:51 +0000 Subject: [PATCH 05/22] Publish explicit portable and AVX2 release variants --- .github/workflows/blackstar-ci.yml | 95 ++++++++++-- .github/workflows/release.yml | 114 +++++++++++---- .gitignore | 1 + README.md | 8 +- docs/BLACKSTAR_RELEASE.md | 28 +++- docs/VERSIONING.md | 6 +- extras/scripts/buildBlackSTARRelease.sh | 45 +++++- extras/scripts/compareBlackSTARReleases.sh | 11 +- extras/scripts/generateBlackSTARSbom.py | 8 +- extras/scripts/inspectBlackSTARBinary.py | 97 +++++++++++++ extras/tests/scripts/testBlackstarVersion.sh | 5 +- extras/tests/scripts/testReleaseVariants.sh | 143 +++++++++++++++++++ source/Parameters.cpp | 2 + source/VERSION | 3 + 14 files changed, 511 insertions(+), 55 deletions(-) create mode 100755 extras/scripts/inspectBlackSTARBinary.py create mode 100755 extras/tests/scripts/testReleaseVariants.sh diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index 2b97e4a8..a6952fb5 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -57,13 +57,16 @@ jobs: extras/maintenance/check_detachment_readiness.py \ extras/maintenance/finalize_successor_repo.py \ extras/maintenance/validate_successor_metadata.py \ + extras/scripts/inspectBlackSTARBinary.py \ extras/scripts/generateBlackSTARSbom.py python3 extras/maintenance/validate_successor_metadata.py - - name: Build BlackSTAR + - name: Build BlackSTAR release variants env: JOBS: "2" - run: extras/scripts/buildBlackSTARRelease.sh + run: | + CPU_TARGET=baseline extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=avx2 extras/scripts/buildBlackSTARRelease.sh - name: Build independent release reproduction env: @@ -72,23 +75,45 @@ jobs: run: | git worktree add --detach "${RUNNER_TEMP}/release-source-2" HEAD cd "${RUNNER_TEMP}/release-source-2" - extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=baseline extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=avx2 extras/scripts/buildBlackSTARRelease.sh - name: Verify cross-path release reproducibility - run: extras/scripts/compareBlackSTARReleases.sh dist "${RUNNER_TEMP}/release-build-2" + run: | + CPU_TARGET=baseline extras/scripts/compareBlackSTARReleases.sh dist "${RUNNER_TEMP}/release-build-2" + CPU_TARGET=avx2 extras/scripts/compareBlackSTARReleases.sh dist "${RUNNER_TEMP}/release-build-2" - name: Verify release identity and OpenMP linkage run: | test "$(source/STAR --version)" = "2.7.11b-blackstar.3" - extras/tests/scripts/testBlackstarVersion.sh + EXPECTED_CPU_TARGET=avx2 extras/tests/scripts/testBlackstarVersion.sh ldd source/STAR | grep -Eq 'libgomp|libomp' - test -f dist/blackstar-1.0.0-linux-x86_64.spdx.json - test -f dist/blackstar-1.0.0-linux-x86_64/LICENSE - test -f dist/blackstar-1.0.0-linux-x86_64/ATTRIBUTION.md + test -f dist/blackstar-1.0.0-linux-x86_64-baseline.spdx.json + test -f dist/blackstar-1.0.0-linux-x86_64-baseline/LICENSE + test -f dist/blackstar-1.0.0-linux-x86_64-baseline/ATTRIBUTION.md + test -f dist/blackstar-1.0.0-linux-x86_64-avx2.spdx.json + grep -Fxq $'cpu_target\tbaseline' \ + dist/blackstar-1.0.0-linux-x86_64-baseline/compatibility.tsv + grep -Fxq $'ymm_instructions\tabsent' \ + dist/blackstar-1.0.0-linux-x86_64-baseline/compatibility.tsv + grep -Fxq $'cpu_target\tavx2' \ + dist/blackstar-1.0.0-linux-x86_64-avx2/compatibility.tsv + grep -Fxq $'ymm_instructions\tpresent' \ + dist/blackstar-1.0.0-linux-x86_64-avx2/compatibility.tsv jq -e ' .spdxVersion == "SPDX-2.3" and (.packages[] | select(.name == "BlackSTAR").versionInfo) == "1.0.0" - ' dist/blackstar-1.0.0-linux-x86_64.spdx.json + ' dist/blackstar-1.0.0-linux-x86_64-baseline.spdx.json + test "$( + jq -r .documentNamespace \ + dist/blackstar-1.0.0-linux-x86_64-baseline.spdx.json + )" != "$( + jq -r .documentNamespace \ + dist/blackstar-1.0.0-linux-x86_64-avx2.spdx.json + )" + + - name: Verify release variant result equivalence + run: extras/tests/scripts/testReleaseVariants.sh - name: Build upstream 2.7.11b compatibility oracle run: | @@ -208,3 +233,55 @@ jobs: if-no-files-found: ignore retention-days: 14 path: ${{ runner.temp }}/starlong-smoke + + release-portability: + runs-on: ubuntu-24.04 + container: ubuntu:20.04 + timeout-minutes: 45 + steps: + - name: Install older-userspace build dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update + apt-get install -y binutils g++ git gzip make python3 zlib1g-dev + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + + - name: Build explicit CPU variants on glibc 2.31 + env: + DIST_DIR: ${{ runner.temp }}/portable-dist + JOBS: "2" + run: | + CPU_TARGET=baseline extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=avx2 extras/scripts/buildBlackSTARRelease.sh + + - name: Verify ABI and ISA floors + env: + DIST_DIR: ${{ runner.temp }}/portable-dist + run: | + for target in baseline avx2; do + compatibility="${DIST_DIR}/blackstar-1.0.0-linux-x86_64-${target}/compatibility.tsv" + minimum_glibc="$(awk -F '\t' '$1=="minimum_glibc" {print $2}' "${compatibility}")" + dpkg --compare-versions "${minimum_glibc}" le "2.31" + done + grep -Fxq $'ymm_instructions\tabsent' \ + "${DIST_DIR}/blackstar-1.0.0-linux-x86_64-baseline/compatibility.tsv" + grep -Fxq $'ymm_instructions\tpresent' \ + "${DIST_DIR}/blackstar-1.0.0-linux-x86_64-avx2/compatibility.tsv" + + - name: Verify release variant result equivalence + env: + DIST_DIR: ${{ runner.temp }}/portable-dist + run: extras/tests/scripts/testReleaseVariants.sh + + - name: Upload portability evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blackstar-portable-release-${{ github.run_id }} + if-no-files-found: error + retention-days: 14 + path: ${{ runner.temp }}/portable-dist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ed365117..7bc55f9d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -14,12 +14,20 @@ permissions: id-token: write jobs: - publish: + build: runs-on: ubuntu-24.04 + container: ubuntu:20.04 timeout-minutes: 60 env: TAG: ${{ inputs.tag }} steps: + - name: Install older-userspace build dependencies + env: + DEBIAN_FRONTEND: noninteractive + run: | + apt-get update + apt-get install -y binutils g++ git gzip make python3 zlib1g-dev + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -35,16 +43,13 @@ jobs: git merge-base --is-ancestor HEAD origin/main test -f "docs/releases/${version}-release-notes.md" - - name: Install build dependencies - run: | - sudo apt-get update - sudo apt-get install -y g++ make zlib1g-dev - - - name: Build first release package + - name: Build first release package set env: DIST_DIR: ${{ runner.temp }}/build-1 JOBS: "2" - run: extras/scripts/buildBlackSTARRelease.sh + run: | + CPU_TARGET=baseline extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=avx2 extras/scripts/buildBlackSTARRelease.sh - name: Build independent reproduction env: @@ -53,10 +58,66 @@ jobs: run: | git worktree add --detach "${RUNNER_TEMP}/source-2" HEAD cd "${RUNNER_TEMP}/source-2" - extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=baseline extras/scripts/buildBlackSTARRelease.sh + CPU_TARGET=avx2 extras/scripts/buildBlackSTARRelease.sh - name: Compare release products - run: extras/scripts/compareBlackSTARReleases.sh "${RUNNER_TEMP}/build-1" "${RUNNER_TEMP}/build-2" + run: | + CPU_TARGET=baseline extras/scripts/compareBlackSTARReleases.sh "${RUNNER_TEMP}/build-1" "${RUNNER_TEMP}/build-2" + CPU_TARGET=avx2 extras/scripts/compareBlackSTARReleases.sh "${RUNNER_TEMP}/build-1" "${RUNNER_TEMP}/build-2" + + - name: Verify release compatibility floors + run: | + version="${TAG#v}" + for target in baseline avx2; do + package="${RUNNER_TEMP}/build-1/blackstar-${version}-linux-x86_64-${target}" + minimum_glibc="$(awk -F '\t' '$1=="minimum_glibc" {print $2}' "${package}/compatibility.tsv")" + dpkg --compare-versions "${minimum_glibc}" le "2.31" + done + grep -Fxq $'ymm_instructions\tabsent' \ + "${RUNNER_TEMP}/build-1/blackstar-${version}-linux-x86_64-baseline/compatibility.tsv" + grep -Fxq $'ymm_instructions\tpresent' \ + "${RUNNER_TEMP}/build-1/blackstar-${version}-linux-x86_64-avx2/compatibility.tsv" + + - name: Verify release variant result equivalence + env: + DIST_DIR: ${{ runner.temp }}/build-1 + run: extras/tests/scripts/testReleaseVariants.sh + + - name: Stage release products for publication + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blackstar-release-products-${{ github.run_id }} + if-no-files-found: error + retention-days: 7 + path: ${{ runner.temp }}/build-1 + + - name: Retain independent reproduction evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: blackstar-release-reproduction-${{ github.run_id }} + if-no-files-found: ignore + retention-days: 30 + path: ${{ runner.temp }}/build-2 + + publish: + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + TAG: ${{ inputs.tag }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + ref: ${{ inputs.tag }} + + - name: Download qualified release products + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: blackstar-release-products-${{ github.run_id }} + path: ${{ runner.temp }}/build-1 - name: Attest release products uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1 @@ -71,26 +132,25 @@ jobs: GH_TOKEN: ${{ github.token }} run: | version="${TAG#v}" - package="blackstar-${version}-linux-x86_64" + baseline="blackstar-${version}-linux-x86_64-baseline" + avx2="blackstar-${version}-linux-x86_64-avx2" gh release create "${TAG}" \ --verify-tag \ --title "BlackSTAR ${version}" \ --notes-file "docs/releases/${version}-release-notes.md" \ - "${RUNNER_TEMP}/build-1/${package}/STAR#BlackSTAR Linux x86-64 executable" \ - "${RUNNER_TEMP}/build-1/${package}.tar.gz#BlackSTAR Linux x86-64 archive" \ - "${RUNNER_TEMP}/build-1/${package}.tar.gz.sha256#Archive SHA-256 sidecar" \ - "${RUNNER_TEMP}/build-1/${package}.spdx.json#SPDX 2.3 SBOM" \ - "${RUNNER_TEMP}/build-1/${package}/build-info.tsv#Reproducible build metadata" \ - "${RUNNER_TEMP}/build-1/${package}/ldd.txt#Runtime linkage metadata" \ + "${RUNNER_TEMP}/build-1/${baseline}/STAR#BlackSTAR baseline Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${baseline}.tar.gz#BlackSTAR baseline Linux x86-64 archive" \ + "${RUNNER_TEMP}/build-1/${baseline}.tar.gz.sha256#Baseline archive SHA-256 sidecar" \ + "${RUNNER_TEMP}/build-1/${baseline}.spdx.json#Baseline SPDX 2.3 SBOM" \ + "${RUNNER_TEMP}/build-1/${baseline}/build-info.tsv#Baseline reproducible build metadata" \ + "${RUNNER_TEMP}/build-1/${baseline}/compatibility.tsv#Baseline compatibility metadata" \ + "${RUNNER_TEMP}/build-1/${baseline}/ldd.txt#Baseline runtime linkage metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}/STAR#BlackSTAR AVX2 Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${avx2}.tar.gz#BlackSTAR AVX2 Linux x86-64 archive" \ + "${RUNNER_TEMP}/build-1/${avx2}.tar.gz.sha256#AVX2 archive SHA-256 sidecar" \ + "${RUNNER_TEMP}/build-1/${avx2}.spdx.json#AVX2 SPDX 2.3 SBOM" \ + "${RUNNER_TEMP}/build-1/${avx2}/build-info.tsv#AVX2 reproducible build metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}/compatibility.tsv#AVX2 compatibility metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}/ldd.txt#AVX2 runtime linkage metadata" \ "LICENSE#MIT license" \ "ATTRIBUTION.md#Upstream lineage and citation" - - - name: Retain release evidence - if: always() - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 - with: - name: blackstar-release-${{ inputs.tag }} - retention-days: 30 - path: | - ${{ runner.temp }}/build-1 - ${{ runner.temp }}/build-2 diff --git a/.gitignore b/.gitignore index a2c3ed80..4e63fc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ *.o Depend.list +Depend.long.list __pycache__/ *.pyc node_modules/ diff --git a/README.md b/README.md index 737e2301..e0f7a89d 100644 --- a/README.md +++ b/README.md @@ -52,9 +52,11 @@ BlackSTAR-specific feature. The latest qualified release is available from the [BlackSTAR releases page](https://github.com/justinblethrow-cloud/blackSTAR/releases). -Release assets include the `STAR` executable, a deterministic Linux x86-64 -archive, checksums, linkage metadata, build provenance, license, and upstream -attribution. +Current release automation produces separately labeled baseline x86-64 and +AVX2 `STAR` executables. Each variant includes a deterministic archive, +checksums, compatibility and linkage metadata, build provenance, license, and +upstream attribution. Use the baseline artifact unless AVX2 support is known +for every target host. Build from source on Linux: diff --git a/docs/BLACKSTAR_RELEASE.md b/docs/BLACKSTAR_RELEASE.md index ddaa601e..4678246e 100644 --- a/docs/BLACKSTAR_RELEASE.md +++ b/docs/BLACKSTAR_RELEASE.md @@ -6,8 +6,10 @@ genome-format boundaries while adding genome-generation, persistent named-sequence insertion, and high-thread alignment improvements. The transitional executable lineage token is `2.7.11b-blackstar.3`. -The `1.0.0` release target is x86-64 Linux. Inherited macOS source support has -not been recertified for the BlackSTAR-specific paths or release builder. +The qualified release target is x86-64 Linux. Release automation emits a +baseline x86-64 artifact and an explicitly labeled AVX2 artifact. Inherited +macOS source support has not been recertified for the BlackSTAR-specific paths +or release builder. See [the acceptance record](BLACKSTAR_ACCEPTANCE.md) for closed audit findings, segregated upstream debt, and measured verification. See @@ -53,6 +55,11 @@ changing measured biological outputs. - Full genome-insert output uses the conventional STAR index file set. The additional `blackstar.complete.tsv` file is ignored by stock STAR. - Overlay and Delta directories are BlackSTAR-specific. Delta v2 and overlay manifest v2 are the only supported development formats. - Overlay and Delta loading requires `NoSharedMemory` and validates the loaded base index against the package identity. +- Baseline release binaries target generic x86-64 and contain no YMM + instructions. AVX2 release binaries are separately named and must contain + AVX2/YMM instructions. +- `STAR --version-json`, `build-info.tsv`, and `compatibility.tsv` independently + record the selected CPU target. ## Release Gates @@ -73,6 +80,9 @@ A release candidate must pass: 8. Broad real-sample base-versus-Delta comparison plus single-end and paired-end synthetic added-reference controls. 9. Downstream shadow through deduplication and counting, with inherited order-sensitive consumers corrected or isolated. 10. Checksum-pinned candidate selection, executable mapping canary, explicit rollback, automatic fallback, and both-invalid state preservation. +11. Independent byte-identical rebuilds of both release variants, ISA-label + validation, an older-userspace ABI floor, and exact alignment and gene-count + equivalence between baseline and AVX2 binaries. The benchmark scripts are evidence collectors, not substitutes for correctness tests. A nonidentical `SA` is never labeled equivalent without mapping-level validation. @@ -84,15 +94,19 @@ Do not describe Delta loading as runtime-free. The `blackstar.1` short-sample sw observed a descriptive 9.22-second mean increase, although alignment and count content remained exact outside the requested added references. -From a clean tagged checkout, build a release package with: +From a clean tagged checkout, build both release variants with: ```bash -JOBS=16 extras/scripts/buildBlackSTARRelease.sh +CPU_TARGET=baseline JOBS=16 extras/scripts/buildBlackSTARRelease.sh +CPU_TARGET=avx2 JOBS=16 extras/scripts/buildBlackSTARRelease.sh ``` -The builder derives `SOURCE_DATE_EPOCH` from the commit, fixes embedded build -provenance, verifies BlackSTAR identity and OpenMP linkage, and writes a -binary, `build-info.tsv`, `ldd.txt`, an SPDX SBOM, a deterministic tarball, and +`CPU_TARGET` defaults to `baseline`. The release workflow builds on Ubuntu +20.04, rejects ambient ISA compiler flags, and enforces a GLIBC requirement no +newer than 2.31. The builder derives `SOURCE_DATE_EPOCH` from the commit, fixes +embedded build provenance, verifies BlackSTAR identity, CPU-target identity, +ISA content, and OpenMP linkage, and writes a binary, `build-info.tsv`, +`compatibility.tsv`, `ldd.txt`, an SPDX SBOM, a deterministic tarball, and SHA-256 checksums under `dist/`. Use `extras/scripts/selectBlackSTAR.sh` for deployment selection. It copies and diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 269eb216..b67f2999 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -11,6 +11,7 @@ machine-readable STAR ancestry and genome-format identities. | STAR compatibility base | `2.7.11b` | Pinned inherited behavior oracle | | BlackSTAR lineage identity | `2.7.11b-blackstar.3` | Transitional executable identity | | Genome format | `2.7.4a` | Conventional index loading compatibility | +| CPU target | `baseline` or `avx2` | Release artifact ISA requirement | These values must not be collapsed into one string. A project release can change without changing the genome format, and a compatibility-base update can @@ -36,8 +37,9 @@ Prereleases use tags such as `v1.1.0-rc.1`. Published tags are immutable. For the 1.x transition, the release package and manifest carry the independent version while `STAR --version` retains a lineage-shaped value for wrappers that expect a STAR-like token. A structured version command and `build-info.tsv` -must report both identities. Changing the legacy `--version` token requires a -separate compatibility survey and release decision. +must report the independent, compatibility, genome-format, and CPU-target +identities. Changing the legacy `--version` token requires a separate +compatibility survey and release decision. ## Compatibility-Base Updates diff --git a/extras/scripts/buildBlackSTARRelease.sh b/extras/scripts/buildBlackSTARRelease.sh index 032b4bd1..e0e2fa66 100755 --- a/extras/scripts/buildBlackSTARRelease.sh +++ b/extras/scripts/buildBlackSTARRelease.sh @@ -10,6 +10,26 @@ cxx="${CXX:-g++}" jobs="${JOBS:-$(nproc 2>/dev/null || echo 1)}" allow_dirty="${ALLOW_DIRTY:-0}" user_cxxflags_extra="${CXXFLAGSEXTRA:-}" +cpu_target="${CPU_TARGET:-baseline}" + +case "${cpu_target}" in + baseline) + target_cxxflags="-march=x86-64 -mtune=generic" + simd_cxxflags="-march=x86-64 -mtune=generic" + ;; + avx2) + target_cxxflags="-march=x86-64 -mtune=generic" + simd_cxxflags="-mavx2 -mtune=generic" + ;; + *) + echo "ERROR: CPU_TARGET must be baseline or avx2" >&2 + exit 2 + ;; +esac +if [[ -n "${CXXFLAGS:-}" || -n "${CPPFLAGS:-}" || -n "${CXXFLAGS_SIMD:-}" ]]; then + echo "ERROR: release builds do not accept CXXFLAGS, CPPFLAGS, or CXXFLAGS_SIMD overrides; use CXXFLAGSEXTRA for non-ISA additions" >&2 + exit 2 +fi cd "${repo_root}" if [[ "${allow_dirty}" != "1" && -n "$(git status --porcelain)" ]]; then @@ -33,7 +53,7 @@ if [[ -z "${compatibility_version}" || -z "${genome_format_version}" ]]; then fi dist_root="${DIST_DIR:-${repo_root}/dist}" -package_name="blackstar-${version}-linux-x86_64" +package_name="blackstar-${version}-linux-x86_64-${cpu_target}" package_final="${dist_root}/${package_name}" archive="${dist_root}/${package_name}.tar.gz" sbom="${dist_root}/${package_name}.spdx.json" @@ -56,7 +76,7 @@ mkdir "${package_dir}" provenance="commit=${commit};tree=$([[ -z "$(git status --porcelain)" ]] && echo clean || echo dirty);release=${version};executable=${executable_version}" path_map_flags="-ffile-prefix-map=${repo_root}=. -fdebug-prefix-map=${repo_root}=. -fmacro-prefix-map=${repo_root}=." -effective_cxxflags_extra="${user_cxxflags_extra:+${user_cxxflags_extra} }${path_map_flags}" +effective_cxxflags_extra="${user_cxxflags_extra:+${user_cxxflags_extra} }${target_cxxflags} -D'BLACKSTAR_CPU_TARGET=\"${cpu_target}\"' ${path_map_flags}" htslib_cflags="-g -Wall -O2 ${path_map_flags}" export SOURCE_DATE_EPOCH="${source_date_epoch}" make -C source CLEAN @@ -66,12 +86,21 @@ make -C source -j"${jobs}" STAR \ BUILD_PLACE="blackstar-reproducible-build" \ GIT_PROVENANCE="${provenance}" \ CXXFLAGSextra="${effective_cxxflags_extra}" \ + CXXFLAGS_SIMD="${simd_cxxflags}" \ LDFLAGSextra="${LDFLAGSEXTRA:-}" if [[ "$(source/STAR --version)" != "${executable_version}" ]]; then echo "ERROR: built binary reports an unexpected version" >&2 exit 1 fi +reported_cpu_target="$( + source/STAR --version-json | + python3 -c 'import json, sys; print(json.load(sys.stdin)["cpu_target"])' +)" +if [[ "${reported_cpu_target}" != "${cpu_target}" ]]; then + echo "ERROR: built binary reports CPU target ${reported_cpu_target}, expected ${cpu_target}" >&2 + exit 1 +fi if ! ldd source/STAR | grep -Eq 'libgomp|libomp'; then echo "ERROR: built binary is not linked to an OpenMP runtime" >&2 exit 1 @@ -80,15 +109,22 @@ fi install -m 0755 source/STAR "${package_dir}/STAR" install -m 0644 LICENSE "${package_dir}/LICENSE" install -m 0644 ATTRIBUTION.md "${package_dir}/ATTRIBUTION.md" +python3 extras/scripts/inspectBlackSTARBinary.py \ + --binary "${package_dir}/STAR" \ + --cpu-target "${cpu_target}" \ + --output "${package_dir}/compatibility.tsv" binary_sha256="$(sha256sum "${package_dir}/STAR" | awk '{print $1}')" +compatibility_sha256="$(sha256sum "${package_dir}/compatibility.tsv" | awk '{print $1}')" license_sha256="$(sha256sum "${package_dir}/LICENSE" | awk '{print $1}')" attribution_sha256="$(sha256sum "${package_dir}/ATTRIBUTION.md" | awk '{print $1}')" compiler_version="$("${cxx}" --version | sed -n '1p')" +build_glibc="$(getconf GNU_LIBC_VERSION 2>/dev/null || echo unknown)" build_utc="$(date -u -d "@${source_date_epoch}" '+%Y-%m-%dT%H:%M:%SZ')" python3 extras/scripts/generateBlackSTARSbom.py \ --repo-root "${repo_root}" \ --binary "${package_dir}/STAR" \ --commit "${commit}" \ + --artifact-variant "linux-x86_64-${cpu_target}" \ --source-date-epoch "${source_date_epoch}" \ --output "${package_dir}/sbom.spdx.json" sbom_sha256="$(sha256sum "${package_dir}/sbom.spdx.json" | awk '{print $1}')" @@ -104,10 +140,15 @@ sbom_sha256="$(sha256sum "${package_dir}/sbom.spdx.json" | awk '{print $1}')" printf 'build_utc\t%s\n' "${build_utc}" printf 'build_place\tblackstar-reproducible-build\n' printf 'compiler\t%s\n' "${compiler_version}" + printf 'build_glibc\t%s\n' "${build_glibc}" + printf 'cpu_target\t%s\n' "${cpu_target}" + printf 'target_cxxflags\t%s\n' "${target_cxxflags}" + printf 'simd_cxxflags\t%s\n' "${simd_cxxflags}" printf 'cxxflags_extra\t%s\n' "${user_cxxflags_extra}" printf 'source_path_mapping\trepository root mapped to .\n' printf 'ldflags_extra\t%s\n' "${LDFLAGSEXTRA:-}" printf 'binary_sha256\t%s\n' "${binary_sha256}" + printf 'compatibility_sha256\t%s\n' "${compatibility_sha256}" printf 'license_sha256\t%s\n' "${license_sha256}" printf 'attribution_sha256\t%s\n' "${attribution_sha256}" printf 'sbom_sha256\t%s\n' "${sbom_sha256}" diff --git a/extras/scripts/compareBlackSTARReleases.sh b/extras/scripts/compareBlackSTARReleases.sh index fc6db02f..78d216eb 100755 --- a/extras/scripts/compareBlackSTARReleases.sh +++ b/extras/scripts/compareBlackSTARReleases.sh @@ -9,7 +9,15 @@ fi script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/../.." && pwd)" version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" -package="blackstar-${version}-linux-x86_64" +cpu_target="${CPU_TARGET:-baseline}" +case "${cpu_target}" in + baseline|avx2) ;; + *) + echo "ERROR: CPU_TARGET must be baseline or avx2" >&2 + exit 2 + ;; +esac +package="blackstar-${version}-linux-x86_64-${cpu_target}" first="$1" second="$2" status=0 @@ -20,6 +28,7 @@ products=( "${package}.spdx.json" "${package}/STAR" "${package}/build-info.tsv" + "${package}/compatibility.tsv" "${package}/ldd.txt" "${package}/LICENSE" "${package}/ATTRIBUTION.md" diff --git a/extras/scripts/generateBlackSTARSbom.py b/extras/scripts/generateBlackSTARSbom.py index ffca47ac..5e198f29 100755 --- a/extras/scripts/generateBlackSTARSbom.py +++ b/extras/scripts/generateBlackSTARSbom.py @@ -33,6 +33,7 @@ def main() -> int: parser.add_argument("--repo-root", type=Path, required=True) parser.add_argument("--binary", type=Path, required=True) parser.add_argument("--commit", required=True) + parser.add_argument("--artifact-variant", required=True) parser.add_argument("--source-date-epoch", type=int, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() @@ -49,7 +50,7 @@ def main() -> int: ).strftime("%Y-%m-%dT%H:%M:%SZ") namespace = ( "https://github.com/justinblethrow-cloud/blackSTAR/" - f"sbom/{blackstar_version}/{args.commit}" + f"sbom/{blackstar_version}/{args.commit}/{args.artifact_variant}" ) document = { "SPDXID": "SPDXRef-DOCUMENT", @@ -59,7 +60,7 @@ def main() -> int: }, "dataLicense": "CC0-1.0", "documentNamespace": namespace, - "name": f"BlackSTAR-{blackstar_version}", + "name": f"BlackSTAR-{blackstar_version}-{args.artifact_variant}", "packages": [ { "SPDXID": "SPDXRef-Package-BlackSTAR", @@ -82,7 +83,8 @@ def main() -> int: "name": "BlackSTAR", "sourceInfo": ( f"Git commit {args.commit}; executable identity " - f"{executable_version}" + f"{executable_version}; artifact variant " + f"{args.artifact_variant}" ), "versionInfo": blackstar_version, }, diff --git a/extras/scripts/inspectBlackSTARBinary.py b/extras/scripts/inspectBlackSTARBinary.py new file mode 100755 index 00000000..98503a01 --- /dev/null +++ b/extras/scripts/inspectBlackSTARBinary.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +"""Record and validate the runtime compatibility boundary of a release ELF.""" + +from __future__ import annotations + +import argparse +from pathlib import Path +import re +import subprocess + + +VERSION_FAMILIES = ("GLIBC", "GLIBCXX", "CXXABI", "GOMP") + + +def run(*args: str) -> str: + return subprocess.run( + args, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + ).stdout + + +def version_key(value: str) -> tuple[int, ...]: + return tuple(int(component) for component in value.split(".")) + + +def highest_symbol_version(symbols: str, family: str) -> str: + pattern = re.compile(rf"\b{re.escape(family)}_(\d+(?:\.\d+)*)\b") + versions = {match.group(1) for match in pattern.finditer(symbols)} + return max(versions, key=version_key) if versions else "none" + + +def elf_field(header: str, name: str) -> str: + pattern = re.compile(rf"^\s*{re.escape(name)}:\s*(.+?)\s*$", re.MULTILINE) + match = pattern.search(header) + if not match: + raise ValueError(f"readelf did not report {name}") + return match.group(1) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--binary", type=Path, required=True) + parser.add_argument( + "--cpu-target", + choices=("baseline", "avx2"), + required=True, + ) + parser.add_argument("--output", type=Path, required=True) + args = parser.parse_args() + + binary = args.binary.resolve() + header = run("readelf", "-h", str(binary)) + symbols = run("objdump", "-T", str(binary)) + disassembly = run("objdump", "-d", str(binary)) + notes = run("readelf", "-n", str(binary)) + + has_ymm = re.search(r"%ymm\d+\b", disassembly) is not None + if args.cpu_target == "baseline" and has_ymm: + raise SystemExit( + "baseline compatibility check failed: binary contains YMM instructions" + ) + if args.cpu_target == "avx2" and not has_ymm: + raise SystemExit( + "AVX2 compatibility check failed: binary contains no YMM instructions" + ) + + isa_property = "not-declared" + for line in notes.splitlines(): + if "x86 ISA needed:" in line: + isa_property = line.split("x86 ISA needed:", 1)[1].strip() + break + + values = [ + ("binary_format", elf_field(header, "Class")), + ("machine", elf_field(header, "Machine")), + ("cpu_target", args.cpu_target), + ("ymm_instructions", "present" if has_ymm else "absent"), + ("gnu_x86_isa_needed", isa_property), + ] + values.extend( + (f"minimum_{family.lower()}", highest_symbol_version(symbols, family)) + for family in VERSION_FAMILIES + ) + + args.output.parent.mkdir(parents=True, exist_ok=True) + with args.output.open("w", encoding="utf-8", newline="\n") as output: + output.write("key\tvalue\n") + for key, value in values: + output.write(f"{key}\t{value}\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/extras/tests/scripts/testBlackstarVersion.sh b/extras/tests/scripts/testBlackstarVersion.sh index 7cad6846..f6cc79b3 100755 --- a/extras/tests/scripts/testBlackstarVersion.sh +++ b/extras/tests/scripts/testBlackstarVersion.sh @@ -13,6 +13,7 @@ expected_executable="$(sed -n 's/^#define STAR_VERSION "\(.*\)"$/\1/p' "${repo_r expected_blackstar="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" expected_compatibility="$(sed -n 's/^#define STAR_COMPATIBILITY_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" expected_genome="$(sed -n 's/^#define BLACKSTAR_GENOME_FORMAT_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" +expected_cpu_target="${EXPECTED_CPU_TARGET:-development}" [[ "$("${star}" --version)" == "${expected_executable}" ]] @@ -21,7 +22,8 @@ python3 - "${version_json}" \ "${expected_blackstar}" \ "${expected_compatibility}" \ "${expected_executable}" \ - "${expected_genome}" <<'PY' + "${expected_genome}" \ + "${expected_cpu_target}" <<'PY' import json import sys @@ -31,6 +33,7 @@ expected = { "star_compatibility_version": sys.argv[3], "executable_version": sys.argv[4], "genome_format_version": sys.argv[5], + "cpu_target": sys.argv[6], } if actual != expected: raise SystemExit(f"version metadata differs: {actual!r} != {expected!r}") diff --git a/extras/tests/scripts/testReleaseVariants.sh b/extras/tests/scripts/testReleaseVariants.sh new file mode 100755 index 00000000..be80e510 --- /dev/null +++ b/extras/tests/scripts/testReleaseVariants.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' "${repo_root}/source/VERSION")" +dist_dir="${DIST_DIR:-${repo_root}/dist}" +baseline_star="${BASELINE_STAR_BIN:-${dist_dir}/blackstar-${version}-linux-x86_64-baseline/STAR}" +avx2_star="${AVX2_STAR_BIN:-${dist_dir}/blackstar-${version}-linux-x86_64-avx2/STAR}" +out_root="${OUT_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/blackstar-release-variants.XXXXXX")}" +keep_output="${KEEP_TEST_OUTPUT:-0}" + +for binary in "${baseline_star}" "${avx2_star}"; do + if [[ ! -x "${binary}" ]]; then + echo "ERROR: release variant is not executable: ${binary}" >&2 + exit 1 + fi +done + +for target in baseline avx2; do + package="${dist_dir}/blackstar-${version}-linux-x86_64-${target}" + binary="${package}/STAR" + reported_target="$( + "${binary}" --version-json | + python3 -c 'import json, sys; print(json.load(sys.stdin)["cpu_target"])' + )" + if [[ "${reported_target}" != "${target}" ]]; then + echo "ERROR: ${target} binary reports CPU target ${reported_target}" >&2 + exit 1 + fi + grep -Fxq "cpu_target ${target}" "${package}/compatibility.tsv" +done + +cleanup() { + if [[ "${keep_output}" == "1" ]]; then + echo "Keeping release variant output: ${out_root}" >&2 + else + rm -rf "${out_root}" + fi +} +trap cleanup EXIT + +if python3 "${repo_root}/extras/scripts/inspectBlackSTARBinary.py" \ + --binary "${avx2_star}" \ + --cpu-target baseline \ + --output "${out_root}/mislabeled-avx2.tsv" \ + > /dev/null 2>&1; then + echo "ERROR: AVX2 binary passed the baseline ISA check" >&2 + exit 1 +fi +if python3 "${repo_root}/extras/scripts/inspectBlackSTARBinary.py" \ + --binary "${baseline_star}" \ + --cpu-target avx2 \ + --output "${out_root}/mislabeled-baseline.tsv" \ + > /dev/null 2>&1; then + echo "ERROR: baseline binary passed the AVX2 ISA check" >&2 + exit 1 +fi + +mkdir -p \ + "${out_root}/index" \ + "${out_root}/baseline" \ + "${out_root}/avx2" +sequence_file="${out_root}/sequence.txt" +genome_fasta="${out_root}/genome.fa" +genome_gtf="${out_root}/genes.gtf" +reads_fastq="${out_root}/reads.fq" + +awk ' + BEGIN { + state=29 + bases="ACGT" + for (ii=0; ii<120000; ii++) { + state=(state*48271)%2147483647 + printf "%s", substr(bases, state%4+1, 1) + } + printf "\n" + } +' > "${sequence_file}" +{ + printf '>chrVariant\n' + fold -w 80 "${sequence_file}" +} > "${genome_fasta}" +printf 'chrVariant\ttest\texon\t1\t120000\t.\t+\t.\tgene_id "variantGene"; transcript_id "variantTranscript";\n' \ + > "${genome_gtf}" +read_sequence="$(cut -c 50001-50100 "${sequence_file}")" +{ + printf '@variant_read\n%s\n+\n' "${read_sequence}" + head -c 100 /dev/zero | tr '\0' I + printf '\n' +} > "${reads_fastq}" + +"${baseline_star}" \ + --runMode genomeGenerate \ + --runThreadN 4 \ + --genomeDir "${out_root}/index" \ + --genomeFastaFiles "${genome_fasta}" \ + --sjdbGTFfile "${genome_gtf}" \ + --sjdbOverhang 99 \ + --genomeSAindexNbases 5 \ + --genomeChrBinNbits 10 \ + --limitGenomeGenerateRAM 200000000 \ + --outFileNamePrefix "${out_root}/build_" \ + > "${out_root}/build.log" 2>&1 + +for target in baseline avx2; do + if [[ "${target}" == "baseline" ]]; then + binary="${baseline_star}" + else + binary="${avx2_star}" + fi + "${binary}" \ + --runThreadN 4 \ + --genomeDir "${out_root}/index" \ + --readFilesIn "${reads_fastq}" \ + --quantMode GeneCounts \ + --outSAMtype SAM \ + --outFileNamePrefix "${out_root}/${target}/" \ + > "${out_root}/${target}.log" 2>&1 + grep -v '^@' "${out_root}/${target}/Aligned.out.sam" \ + > "${out_root}/${target}/Aligned.body.sam" +done + +diff -u \ + "${out_root}/baseline/Aligned.body.sam" \ + "${out_root}/avx2/Aligned.body.sam" +diff -u \ + "${out_root}/baseline/SJ.out.tab" \ + "${out_root}/avx2/SJ.out.tab" +diff -u \ + "${out_root}/baseline/ReadsPerGene.out.tab" \ + "${out_root}/avx2/ReadsPerGene.out.tab" +if ! grep -Eq 'Number of input reads[[:space:]]*\|[[:space:]]*1$' \ + "${out_root}/baseline/Log.final.out"; then + echo "ERROR: baseline release did not process the fixture read" >&2 + exit 1 +fi + +printf 'check\tstatus\n' +printf 'baseline_alignment\tpass\n' +printf 'avx2_alignment\tpass\n' +printf 'variant_alignment_equivalence\tpass\n' +printf 'variant_gene_count_equivalence\tpass\n' diff --git a/source/Parameters.cpp b/source/Parameters.cpp index 94688ad0..5517dbe9 100755 --- a/source/Parameters.cpp +++ b/source/Parameters.cpp @@ -374,6 +374,8 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters << "\",\"executable_version\":\"" << STAR_VERSION << "\",\"genome_format_version\":\"" << BLACKSTAR_GENOME_FORMAT_VERSION + << "\",\"cpu_target\":\"" + << BLACKSTAR_CPU_TARGET << "\"}" << std::endl; exit(0); }; diff --git a/source/VERSION b/source/VERSION index 682dba38..d8eebd05 100644 --- a/source/VERSION +++ b/source/VERSION @@ -2,3 +2,6 @@ #define BLACKSTAR_VERSION "1.0.0" #define STAR_COMPATIBILITY_VERSION "2.7.11b" #define BLACKSTAR_GENOME_FORMAT_VERSION "2.7.4a" +#ifndef BLACKSTAR_CPU_TARGET +#define BLACKSTAR_CPU_TARGET "development" +#endif From 387101d6c73d3fef6e4dfcf8c6501b2387cc0eb2 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 20:50:08 +0000 Subject: [PATCH 06/22] Qualify specialized and long-read alignment modes --- .github/workflows/blackstar-ci.yml | 19 +- .gitignore | 1 + extras/tests/scripts/testSTARlong.sh | 57 +- extras/tests/scripts/testSpecializedModes.sh | 636 +++++++++++++++++++ 4 files changed, 710 insertions(+), 3 deletions(-) create mode 100755 extras/tests/scripts/testSpecializedModes.sh diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index a6952fb5..e3474fa5 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -26,7 +26,7 @@ jobs: - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install -y g++ jq make python3-yaml zlib1g-dev + sudo apt-get install -y g++ jq make python3-yaml samtools zlib1g-dev - name: Install architecture renderer run: npm ci --prefix extras/docs @@ -121,6 +121,13 @@ jobs: make -C "${RUNNER_TEMP}/star-upstream-2.7.11b/source" -j2 STAR echo "UPSTREAM_STAR_BIN=${RUNNER_TEMP}/star-upstream-2.7.11b/source/STAR" >> "${GITHUB_ENV}" + - name: Run specialized-mode compatibility tests + env: + KEEP_TEST_OUTPUT: "1" + OUT_DIR: ${{ runner.temp }}/specialized-modes + THREADS: "4" + run: extras/tests/scripts/testSpecializedModes.sh + - name: Run focused sanitizer tests run: | extras/tests/scripts/testAlignmentThreadAffinity.sh @@ -168,6 +175,7 @@ jobs: ${{ runner.temp }}/release-build-2 ${{ runner.temp }}/genome-insert-hardening ${{ runner.temp }}/saindex-strategies + ${{ runner.temp }}/specialized-modes compiler-build: name: compiler-${{ matrix.name }} @@ -205,6 +213,8 @@ jobs: timeout-minutes: 35 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 - name: Install build dependencies run: | @@ -218,11 +228,18 @@ jobs: test -x source/STAR test -x source/STARlong + - name: Build official STARlong compatibility oracle + run: | + git worktree add --detach "${RUNNER_TEMP}/star-upstream-long" 2.7.11b + make -C "${RUNNER_TEMP}/star-upstream-long/source" -j2 STARlong + echo "UPSTREAM_STARLONG_BIN=${RUNNER_TEMP}/star-upstream-long/source/STARlong" >> "${GITHUB_ENV}" + - name: Exercise STARlong-only code and high-thread chunk sizing env: THREADS: "96" KEEP_TEST_OUTPUT: "1" OUT_DIR: ${{ runner.temp }}/starlong-smoke + REQUIRE_UPSTREAM_STARLONG: "1" run: extras/tests/scripts/testSTARlong.sh - name: Upload STARlong smoke evidence diff --git a/.gitignore b/.gitignore index 4e63fc0e..d8091268 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ extras/doc-latex/*.toc # Don't track the STAR binary once it has being built source/STAR +source/STARlong source/parametersDefault.xxd /dist/ /benchmarks/labs/ diff --git a/extras/tests/scripts/testSTARlong.sh b/extras/tests/scripts/testSTARlong.sh index db6ef317..158c070e 100755 --- a/extras/tests/scripts/testSTARlong.sh +++ b/extras/tests/scripts/testSTARlong.sh @@ -4,6 +4,8 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/../../.." && pwd)" star_long="${STARLONG_BIN:-${repo_root}/source/STARlong}" +upstream_star_long="${UPSTREAM_STARLONG_BIN:-}" +require_upstream="${REQUIRE_UPSTREAM_STARLONG:-0}" threads="${THREADS:-96}" out_root="${OUT_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/blackstar-starlong-smoke.XXXXXX")}" keep_output="${KEEP_TEST_OUTPUT:-0}" @@ -12,6 +14,19 @@ if [[ ! -x "${star_long}" ]]; then echo "ERROR: STARLONG_BIN is not executable: ${star_long}" >&2 exit 1 fi +if [[ -n "${upstream_star_long}" && ! -x "${upstream_star_long}" ]]; then + echo "ERROR: UPSTREAM_STARLONG_BIN is not executable: ${upstream_star_long}" >&2 + exit 1 +fi +if [[ -n "${upstream_star_long}" ]] && + [[ "$("${upstream_star_long}" --version)" != "2.7.11b" ]]; then + echo "ERROR: UPSTREAM_STARLONG_BIN does not report official STAR 2.7.11b" >&2 + exit 1 +fi +if [[ "${require_upstream}" == "1" && -z "${upstream_star_long}" ]]; then + echo "ERROR: REQUIRE_UPSTREAM_STARLONG=1 requires UPSTREAM_STARLONG_BIN" >&2 + exit 1 +fi cleanup() { if [[ "${keep_output}" == "1" ]]; then @@ -22,7 +37,10 @@ cleanup() { } trap cleanup EXIT -mkdir -p "${out_root}/index" "${out_root}/alignment" +mkdir -p \ + "${out_root}/index" \ + "${out_root}/alignment" \ + "${out_root}/upstream-alignment" genome_fasta="${out_root}/genome.fa" reads_fastq="${out_root}/reads.fq" sequence_file="${out_root}/sequence.txt" @@ -64,7 +82,8 @@ long_read="$(cut -c 20001-22000 "${sequence_file}")" --runThreadN "${threads}" \ --genomeDir "${out_root}/index" \ --readFilesIn "${reads_fastq}" \ - --outSAMtype None \ + --outSAMtype SAM \ + --outSAMattributes Standard \ --outSJtype None \ --outFileNamePrefix "${out_root}/alignment/" \ > "${out_root}/alignment.log" 2>&1 @@ -90,8 +109,42 @@ if ! grep -Eq 'Number of input reads[[:space:]]*\|[[:space:]]*1$' \ echo "ERROR: STARlong did not process the 2000 nt smoke read" >&2 exit 1 fi +if ! awk '$0 !~ /^@/ && $6=="2000M" {found=1} END {exit found ? 0 : 1}' \ + "${out_root}/alignment/Aligned.out.sam"; then + echo "ERROR: STARlong did not emit the expected full-length alignment" >&2 + exit 1 +fi + +upstream_status="not_run" +if [[ -n "${upstream_star_long}" ]]; then + "${upstream_star_long}" \ + --runThreadN 1 \ + --genomeDir "${out_root}/index" \ + --readFilesIn "${reads_fastq}" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outSJtype None \ + --outFileNamePrefix "${out_root}/upstream-alignment/" \ + > "${out_root}/upstream-alignment.log" 2>&1 + awk '$0 !~ /^@/' "${out_root}/alignment/Aligned.out.sam" | + LC_ALL=C sort > "${out_root}/alignment.body.sorted.sam" + awk '$0 !~ /^@/' "${out_root}/upstream-alignment/Aligned.out.sam" | + LC_ALL=C sort > "${out_root}/upstream-alignment.body.sorted.sam" + if ! cmp -s \ + "${out_root}/upstream-alignment.body.sorted.sam" \ + "${out_root}/alignment.body.sorted.sam"; then + echo "ERROR: STARlong alignment differs from official STARlong" >&2 + diff -u \ + "${out_root}/upstream-alignment.body.sorted.sam" \ + "${out_root}/alignment.body.sorted.sam" | + sed -n '1,160p' >&2 || true + exit 1 + fi + upstream_status="pass" +fi printf 'check\tstatus\n' printf 'clean_long_read_build\tpass\n' printf 'long_read_2000nt_input\tpass\n' printf 'high_thread_chunk_floor\tpass\n' +printf 'upstream_long_read_equivalence\t%s\n' "${upstream_status}" diff --git a/extras/tests/scripts/testSpecializedModes.sh b/extras/tests/scripts/testSpecializedModes.sh new file mode 100755 index 00000000..86a5d4bf --- /dev/null +++ b/extras/tests/scripts/testSpecializedModes.sh @@ -0,0 +1,636 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +star_bin="${STAR_BIN:-${repo_root}/source/STAR}" +upstream_star="${UPSTREAM_STAR_BIN:-}" +threads="${THREADS:-4}" +keep_output="${KEEP_TEST_OUTPUT:-0}" +out_root="${OUT_DIR:-$(mktemp -d "${TMPDIR:-/tmp}/blackstar-specialized-modes.XXXXXX")}" + +if [[ ! -x "${star_bin}" ]]; then + echo "ERROR: STAR_BIN is not executable: ${star_bin}" >&2 + exit 1 +fi +if [[ -z "${upstream_star}" || ! -x "${upstream_star}" ]]; then + echo "ERROR: UPSTREAM_STAR_BIN must name an executable official STAR oracle" >&2 + exit 1 +fi +if [[ "$("${upstream_star}" --version)" != "2.7.11b" ]]; then + echo "ERROR: UPSTREAM_STAR_BIN does not report official STAR 2.7.11b" >&2 + exit 1 +fi +if ! command -v samtools > /dev/null 2>&1; then + echo "ERROR: samtools is required to compare transcriptome BAM output" >&2 + exit 1 +fi + +base_index="${out_root}/base-index" +shared_cleanup_prefix="${out_root}/shared-cleanup_" + +remove_shared_genome() { + if [[ -d "${base_index}" ]]; then + "${star_bin}" \ + --genomeDir "${base_index}" \ + --genomeLoad Remove \ + --outFileNamePrefix "${shared_cleanup_prefix}" \ + > /dev/null 2>&1 || true + fi +} + +cleanup() { + remove_shared_genome + if [[ "${keep_output}" == "1" ]]; then + echo "Keeping specialized-mode output: ${out_root}" >&2 + else + rm -rf "${out_root}" + fi +} +trap cleanup EXIT + +mkdir -p "${out_root}/inputs" "${base_index}" +python3 - "${out_root}/inputs" <<'PY' +from __future__ import annotations + +import hashlib +from pathlib import Path +import sys + + +root = Path(sys.argv[1]) + + +def make_sequence(length: int, seed: int) -> list[str]: + bases = "ACGT" + sequence: list[str] = [] + counter = 0 + seed_bytes = seed.to_bytes(8, byteorder="little", signed=False) + while len(sequence) < length: + digest = hashlib.sha256( + seed_bytes + counter.to_bytes(8, byteorder="little", signed=False) + ).digest() + for value in digest: + for shift in (0, 2, 4, 6): + sequence.append(bases[(value >> shift) & 3]) + if len(sequence) == length: + return sequence + counter += 1 + return sequence + + +def subseq(sequence: list[str], start: int, length: int) -> str: + return "".join(sequence[start - 1 : start - 1 + length]) + + +def reverse_complement(sequence: str) -> str: + return sequence.translate(str.maketrans("ACGT", "TGCA"))[::-1] + + +def write_fastq(path: Path, reads: list[tuple[str, str]]) -> None: + with path.open("w", encoding="ascii", newline="\n") as handle: + for name, sequence in reads: + handle.write(f"@{name}\n{sequence}\n+\n{'I' * len(sequence)}\n") + + +chr_a = make_sequence(160000, 1729) +chr_b = make_sequence(160000, 8675309) + +# Canonical GT/AG introns for one annotated and one unannotated splice. +for sequence, exon_end, next_exon_start in ( + (chr_a, 10300, 11001), + (chr_a, 50200, 51001), +): + sequence[exon_end : exon_end + 2] = list("GT") + sequence[next_exon_start - 3 : next_exon_start - 1] = list("AG") + +with (root / "genome.fa").open("w", encoding="ascii", newline="\n") as handle: + for name, sequence in (("chrA", chr_a), ("chrB", chr_b)): + handle.write(f">{name}\n") + joined = "".join(sequence) + for offset in range(0, len(joined), 80): + handle.write(joined[offset : offset + 80] + "\n") + +(root / "genes.gtf").write_text( + "\n".join( + ( + 'chrA\tfixture\texon\t10001\t10300\t.\t+\t.\tgene_id "geneA"; transcript_id "txA";', + 'chrA\tfixture\texon\t11001\t11300\t.\t+\t.\tgene_id "geneA"; transcript_id "txA";', + 'chrA\tfixture\texon\t90001\t90600\t.\t+\t.\tgene_id "geneW"; transcript_id "txW";', + 'chrB\tfixture\texon\t30001\t30600\t.\t+\t.\tgene_id "geneB"; transcript_id "txB";', + ) + ) + + "\n", + encoding="ascii", +) + +paired_r1 = [ + ("pair_exonic", subseq(chr_a, 10050, 100)), + ( + "pair_spliced", + subseq(chr_a, 10241, 60) + subseq(chr_a, 11001, 40), + ), + ("pair_gene_w", subseq(chr_a, 90101, 100)), +] +paired_r2 = [ + ("pair_exonic", reverse_complement(subseq(chr_a, 11150, 100))), + ("pair_spliced", reverse_complement(subseq(chr_a, 11121, 100))), + ("pair_gene_w", reverse_complement(subseq(chr_a, 90301, 100))), +] +write_fastq(root / "paired_R1.fq", paired_r1) +write_fastq(root / "paired_R2.fq", paired_r2) + +novel_reads: list[tuple[str, str]] = [] +for index in range(8): + left_length = 46 + index + right_length = 100 - left_length + novel_reads.append( + ( + f"novel_splice_{index + 1}", + subseq(chr_a, 50201 - left_length, left_length) + + subseq(chr_a, 51001, right_length), + ) + ) +write_fastq(root / "novel_splice.fq", novel_reads) + +chimeric_sequence = subseq(chr_a, 70001, 70) + subseq(chr_b, 80001, 70) +write_fastq(root / "chimeric.fq", [("chrA_chrB_fusion", chimeric_sequence)]) + +variant_position = 90250 +reference = chr_a[variant_position - 1] +alternate = next(base for base in "ACGT" if base != reference) +variant_start = 90201 +reference_read = subseq(chr_a, variant_start, 100) +alternate_read = list(reference_read) +alternate_read[variant_position - variant_start] = alternate +alternate_read_text = "".join(alternate_read) +write_fastq( + root / "wasp.fq", + (("wasp_reference", reference_read), ("wasp_alternate", alternate_read_text)), +) +write_fastq(root / "transform.fq", [("haploid_alternate", alternate_read_text)]) + +(root / "variants.vcf").write_text( + "##fileformat=VCFv4.2\n" + "##FORMAT=\n" + "#CHROM\tPOS\tID\tREF\tALT\tQUAL\tFILTER\tINFO\tFORMAT\tfixture\n" + f"chrA\t{variant_position}\t.\t{reference}\t{alternate}\t.\tPASS\t.\tGT\t0/1\n", + encoding="ascii", +) + +barcode = "ACGTACGTACGTACGT" +umis = ("AACCGGTTAA", "AACCGGTTAC", "TTGGAACCTT") +solo_cdna = [ + (f"solo_{index + 1}", subseq(chr_a, 90101 + index * 30, 100)) + for index in range(len(umis)) +] +solo_barcode = [ + (f"solo_{index + 1}", barcode + umi) + for index, umi in enumerate(umis) +] +write_fastq(root / "solo_cdna.fq", solo_cdna) +write_fastq(root / "solo_barcode.fq", solo_barcode) +(root / "solo_whitelist.txt").write_text(barcode + "\n", encoding="ascii") + +(root / "input.sam").write_text( + "@HD\tVN:1.6\tSO:unsorted\n" + "@SQ\tSN:chrA\tLN:160000\n" + f"sam_input\t4\t*\t0\t0\t*\t*\t0\t0\t{reference_read}\t" + f"{'I' * len(reference_read)}\tRG:Z:fixture\tZZ:Z:preserved\n", + encoding="ascii", +) +PY + +genome_fasta="${out_root}/inputs/genome.fa" +genome_gtf="${out_root}/inputs/genes.gtf" +variants_vcf="${out_root}/inputs/variants.vcf" + +"${star_bin}" \ + --runMode genomeGenerate \ + --runThreadN "${threads}" \ + --genomeDir "${base_index}" \ + --genomeFastaFiles "${genome_fasta}" \ + --sjdbGTFfile "${genome_gtf}" \ + --sjdbOverhang 99 \ + --genomeSAindexNbases 6 \ + --genomeChrBinNbits 10 \ + --limitGenomeGenerateRAM 500000000 \ + --outFileNamePrefix "${out_root}/base-build_" \ + > "${out_root}/base-build.log" 2>&1 + +normalize_sam() { + local input="$1" + local output="$2" + awk '$0 !~ /^@/' "${input}" | LC_ALL=C sort > "${output}" +} + +normalize_bam() { + local input="$1" + local output="$2" + samtools view "${input}" | LC_ALL=C sort > "${output}" +} + +normalize_text() { + local input="$1" + local output="$2" + LC_ALL=C sort "${input}" > "${output}" +} + +compare_exact() { + local label="$1" + local first="$2" + local second="$3" + if ! cmp -s "${first}" "${second}"; then + echo "ERROR: ${label} differs between official STAR and BlackSTAR" >&2 + diff -u "${first}" "${second}" | sed -n '1,160p' >&2 || true + exit 1 + fi +} + +prepare_mode() { + local mode="$1" + mkdir -p "${out_root}/${mode}/upstream" "${out_root}/${mode}/blackstar" +} + +compare_sam_outputs() { + local mode="$1" + normalize_sam \ + "${out_root}/${mode}/upstream/Aligned.out.sam" \ + "${out_root}/${mode}/upstream/Aligned.body.sorted.sam" + normalize_sam \ + "${out_root}/${mode}/blackstar/Aligned.out.sam" \ + "${out_root}/${mode}/blackstar/Aligned.body.sorted.sam" + compare_exact \ + "${mode} SAM records" \ + "${out_root}/${mode}/upstream/Aligned.body.sorted.sam" \ + "${out_root}/${mode}/blackstar/Aligned.body.sorted.sam" +} + +prepare_mode paired +"${upstream_star}" \ + --runThreadN 1 \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/paired_R1.fq" "${out_root}/inputs/paired_R2.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --quantMode TranscriptomeSAM GeneCounts \ + --outFileNamePrefix "${out_root}/paired/upstream/" \ + > "${out_root}/paired/upstream.stdout" 2>&1 +"${star_bin}" \ + --runThreadN "${threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/paired_R1.fq" "${out_root}/inputs/paired_R2.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --quantMode TranscriptomeSAM GeneCounts \ + --outFileNamePrefix "${out_root}/paired/blackstar/" \ + > "${out_root}/paired/blackstar.stdout" 2>&1 +compare_sam_outputs paired +normalize_bam \ + "${out_root}/paired/upstream/Aligned.toTranscriptome.out.bam" \ + "${out_root}/paired/upstream/transcriptome.sorted.sam" +normalize_bam \ + "${out_root}/paired/blackstar/Aligned.toTranscriptome.out.bam" \ + "${out_root}/paired/blackstar/transcriptome.sorted.sam" +compare_exact \ + "paired transcriptome BAM records" \ + "${out_root}/paired/upstream/transcriptome.sorted.sam" \ + "${out_root}/paired/blackstar/transcriptome.sorted.sam" +compare_exact \ + "paired gene counts" \ + "${out_root}/paired/upstream/ReadsPerGene.out.tab" \ + "${out_root}/paired/blackstar/ReadsPerGene.out.tab" +if ! grep -Eq $'\t[0-9]+M[0-9]+N[0-9]+M\t' \ + "${out_root}/paired/blackstar/Aligned.body.sorted.sam"; then + echo "ERROR: paired whole-transcript fixture produced no spliced alignment" >&2 + exit 1 +fi + +for mode in twopass bysjout; do + prepare_mode "${mode}" + extra_args=() + if [[ "${mode}" == "twopass" ]]; then + extra_args+=(--twopassMode Basic) + else + extra_args+=(--outFilterType BySJout) + fi + "${upstream_star}" \ + --runThreadN 1 \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/${mode}/upstream/" \ + "${extra_args[@]}" \ + > "${out_root}/${mode}/upstream.stdout" 2>&1 + "${star_bin}" \ + --runThreadN "${threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/${mode}/blackstar/" \ + "${extra_args[@]}" \ + > "${out_root}/${mode}/blackstar.stdout" 2>&1 + compare_sam_outputs "${mode}" + normalize_text \ + "${out_root}/${mode}/upstream/SJ.out.tab" \ + "${out_root}/${mode}/upstream/SJ.sorted.tab" + normalize_text \ + "${out_root}/${mode}/blackstar/SJ.out.tab" \ + "${out_root}/${mode}/blackstar/SJ.sorted.tab" + compare_exact \ + "${mode} splice junctions" \ + "${out_root}/${mode}/upstream/SJ.sorted.tab" \ + "${out_root}/${mode}/blackstar/SJ.sorted.tab" + if [[ ! -s "${out_root}/${mode}/blackstar/SJ.sorted.tab" ]]; then + echo "ERROR: ${mode} fixture produced no splice junction" >&2 + exit 1 + fi +done + +prepare_mode chimeric +for implementation in upstream blackstar; do + if [[ "${implementation}" == "upstream" ]]; then + binary="${upstream_star}" + mode_threads=1 + else + binary="${star_bin}" + mode_threads="${threads}" + fi + "${binary}" \ + --runThreadN "${mode_threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/chimeric.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --chimSegmentMin 30 \ + --chimJunctionOverhangMin 20 \ + --chimScoreMin 30 \ + --chimScoreDropMax 100 \ + --chimScoreSeparation 0 \ + --chimFilter None \ + --chimOutType Junctions SeparateSAMold \ + --outFileNamePrefix "${out_root}/chimeric/${implementation}/" \ + > "${out_root}/chimeric/${implementation}.stdout" 2>&1 +done +compare_sam_outputs chimeric +normalize_sam \ + "${out_root}/chimeric/upstream/Chimeric.out.sam" \ + "${out_root}/chimeric/upstream/Chimeric.body.sorted.sam" +normalize_sam \ + "${out_root}/chimeric/blackstar/Chimeric.out.sam" \ + "${out_root}/chimeric/blackstar/Chimeric.body.sorted.sam" +compare_exact \ + "chimeric SAM records" \ + "${out_root}/chimeric/upstream/Chimeric.body.sorted.sam" \ + "${out_root}/chimeric/blackstar/Chimeric.body.sorted.sam" +normalize_text \ + "${out_root}/chimeric/upstream/Chimeric.out.junction" \ + "${out_root}/chimeric/upstream/Chimeric.sorted.junction" +normalize_text \ + "${out_root}/chimeric/blackstar/Chimeric.out.junction" \ + "${out_root}/chimeric/blackstar/Chimeric.sorted.junction" +compare_exact \ + "chimeric junctions" \ + "${out_root}/chimeric/upstream/Chimeric.sorted.junction" \ + "${out_root}/chimeric/blackstar/Chimeric.sorted.junction" +if [[ ! -s "${out_root}/chimeric/blackstar/Chimeric.sorted.junction" ]]; then + echo "ERROR: chimeric fixture produced no fusion junction" >&2 + exit 1 +fi + +prepare_mode wasp +for implementation in upstream blackstar; do + if [[ "${implementation}" == "upstream" ]]; then + binary="${upstream_star}" + mode_threads=1 + else + binary="${star_bin}" + mode_threads="${threads}" + fi + "${binary}" \ + --runThreadN "${mode_threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/wasp.fq" \ + --varVCFfile "${variants_vcf}" \ + --waspOutputMode SAMtag \ + --outSAMtype BAM Unsorted \ + --outSAMattributes NH HI AS nM vW \ + --outFileNamePrefix "${out_root}/wasp/${implementation}/" \ + > "${out_root}/wasp/${implementation}.stdout" 2>&1 +done +normalize_bam \ + "${out_root}/wasp/upstream/Aligned.out.bam" \ + "${out_root}/wasp/upstream/Aligned.body.sorted.sam" +normalize_bam \ + "${out_root}/wasp/blackstar/Aligned.out.bam" \ + "${out_root}/wasp/blackstar/Aligned.body.sorted.sam" +compare_exact \ + "WASP BAM records" \ + "${out_root}/wasp/upstream/Aligned.body.sorted.sam" \ + "${out_root}/wasp/blackstar/Aligned.body.sorted.sam" +if ! grep -Fq $'\tvW:i:' "${out_root}/wasp/blackstar/Aligned.body.sorted.sam"; then + echo "ERROR: WASP fixture produced no vW tag" >&2 + exit 1 +fi + +prepare_mode solo +for implementation in upstream blackstar; do + if [[ "${implementation}" == "upstream" ]]; then + binary="${upstream_star}" + mode_threads=1 + else + binary="${star_bin}" + mode_threads="${threads}" + fi + "${binary}" \ + --runThreadN "${mode_threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn \ + "${out_root}/inputs/solo_cdna.fq" \ + "${out_root}/inputs/solo_barcode.fq" \ + --soloType CB_UMI_Simple \ + --soloCBwhitelist "${out_root}/inputs/solo_whitelist.txt" \ + --soloCBmatchWLtype Exact \ + --soloUMIdedup Exact \ + --soloCellFilter None \ + --soloStrand Forward \ + --soloFeatures Gene \ + --outSAMtype SAM \ + --outSAMattributes Standard CR UR CB UB GX GN \ + --outFileNamePrefix "${out_root}/solo/${implementation}/" \ + > "${out_root}/solo/${implementation}.stdout" 2>&1 +done +compare_sam_outputs solo +for file in barcodes.tsv features.tsv matrix.mtx; do + compare_exact \ + "STARsolo raw ${file}" \ + "${out_root}/solo/upstream/Solo.out/Gene/raw/${file}" \ + "${out_root}/solo/blackstar/Solo.out/Gene/raw/${file}" +done +if ! awk ' + /^%/ {next} + {line++} + line>1 && $3>0 {found=1} + END {exit found ? 0 : 1} +' "${out_root}/solo/blackstar/Solo.out/Gene/raw/matrix.mtx"; then + echo "ERROR: STARsolo fixture produced no gene/UMI count" >&2 + exit 1 +fi + +prepare_mode sam-input +for implementation in upstream blackstar; do + if [[ "${implementation}" == "upstream" ]]; then + binary="${upstream_star}" + mode_threads=1 + else + binary="${star_bin}" + mode_threads="${threads}" + fi + "${binary}" \ + --runThreadN "${mode_threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/input.sam" \ + --readFilesType SAM SE \ + --readFilesSAMattrKeep All \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/sam-input/${implementation}/" \ + > "${out_root}/sam-input/${implementation}.stdout" 2>&1 +done +compare_sam_outputs sam-input +if ! grep -Fq $'\tRG:Z:fixture\tZZ:Z:preserved' \ + "${out_root}/sam-input/blackstar/Aligned.body.sorted.sam"; then + echo "ERROR: SAM-input fixture did not preserve optional attributes" >&2 + exit 1 +fi + +prepare_mode shared +"${star_bin}" \ + --runThreadN "${threads}" \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/shared/blackstar-private_" \ + > "${out_root}/shared/blackstar-private.stdout" 2>&1 +"${upstream_star}" \ + --runThreadN 1 \ + --genomeDir "${base_index}" \ + --genomeLoad LoadAndKeep \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/shared/upstream-keep_" \ + > "${out_root}/shared/upstream-keep.stdout" 2>&1 +"${star_bin}" \ + --runThreadN "${threads}" \ + --genomeDir "${base_index}" \ + --genomeLoad LoadAndRemove \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/shared/blackstar-remove_" \ + > "${out_root}/shared/blackstar-remove.stdout" 2>&1 +"${star_bin}" \ + --runThreadN "${threads}" \ + --genomeDir "${base_index}" \ + --genomeLoad LoadAndKeep \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/shared/blackstar-keep_" \ + > "${out_root}/shared/blackstar-keep.stdout" 2>&1 +"${upstream_star}" \ + --runThreadN 1 \ + --genomeDir "${base_index}" \ + --genomeLoad LoadAndRemove \ + --readFilesIn "${out_root}/inputs/novel_splice.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/shared/upstream-remove_" \ + > "${out_root}/shared/upstream-remove.stdout" 2>&1 +for label in \ + blackstar-private \ + upstream-keep \ + blackstar-remove \ + blackstar-keep \ + upstream-remove; do + normalize_sam \ + "${out_root}/shared/${label}_Aligned.out.sam" \ + "${out_root}/shared/${label}.body.sorted.sam" +done +for label in upstream-keep blackstar-remove blackstar-keep upstream-remove; do + compare_exact \ + "shared-memory ${label} records" \ + "${out_root}/shared/blackstar-private.body.sorted.sam" \ + "${out_root}/shared/${label}.body.sorted.sam" +done + +mkdir -p \ + "${out_root}/transform/upstream-index" \ + "${out_root}/transform/blackstar-index" \ + "${out_root}/transform/upstream" \ + "${out_root}/transform/blackstar" +for implementation in upstream blackstar; do + if [[ "${implementation}" == "upstream" ]]; then + binary="${upstream_star}" + else + binary="${star_bin}" + fi + "${binary}" \ + --runMode genomeGenerate \ + --runThreadN 1 \ + --genomeDir "${out_root}/transform/${implementation}-index" \ + --genomeFastaFiles "${genome_fasta}" \ + --sjdbGTFfile "${genome_gtf}" \ + --sjdbOverhang 99 \ + --genomeTransformType Haploid \ + --genomeTransformVCF "${variants_vcf}" \ + --genomeSAindexNbases 6 \ + --genomeChrBinNbits 10 \ + --limitGenomeGenerateRAM 500000000 \ + --outFileNamePrefix "${out_root}/transform/${implementation}-build_" \ + > "${out_root}/transform/${implementation}-build.stdout" 2>&1 + "${binary}" \ + --runThreadN 1 \ + --genomeDir "${out_root}/transform/${implementation}-index" \ + --readFilesIn "${out_root}/inputs/transform.fq" \ + --genomeTransformOutput SAM SJ Quant \ + --quantMode GeneCounts \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --outFileNamePrefix "${out_root}/transform/${implementation}/" \ + > "${out_root}/transform/${implementation}.stdout" 2>&1 +done +compare_sam_outputs transform +compare_exact \ + "transformed-genome gene counts" \ + "${out_root}/transform/upstream/ReadsPerGene.out.tab" \ + "${out_root}/transform/blackstar/ReadsPerGene.out.tab" +normalize_text \ + "${out_root}/transform/upstream/SJ.out.tab" \ + "${out_root}/transform/upstream/SJ.sorted.tab" +normalize_text \ + "${out_root}/transform/blackstar/SJ.out.tab" \ + "${out_root}/transform/blackstar/SJ.sorted.tab" +compare_exact \ + "transformed-genome splice junctions" \ + "${out_root}/transform/upstream/SJ.sorted.tab" \ + "${out_root}/transform/blackstar/SJ.sorted.tab" + +cat <<'EOF_RESULTS' +check status +paired_fragmented_alignment pass +transcriptome_bam pass +gene_counts pass +two_pass_mapping pass +bysjout_filtering pass +chimeric_detection pass +wasp_filtering pass +starsolo_gene_umi_counts pass +sam_input pass +shared_memory_cross_binary_lifecycle pass +haploid_genome_transform pass +EOF_RESULTS From 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 21:22:14 +0000 Subject: [PATCH 07/22] Add cross-workload qualification harness --- .github/workflows/blackstar-ci.yml | 7 +- extras/benchmarks/README.md | 35 ++ extras/benchmarks/compareAlignmentRuns.py | 16 + .../benchmarks/compareGeneralizationRuns.py | 243 ++++++++++ extras/benchmarks/makeFastqSubset.py | 101 ++++ extras/benchmarks/runGeneralizationMode.sh | 298 ++++++++++++ extras/benchmarks/runGeneralizationPairs.py | 442 ++++++++++++++++++ extras/tests/scripts/testBenchmarkHarness.sh | 40 ++ .../scripts/testGeneralizationHarness.sh | 144 ++++++ 9 files changed, 1325 insertions(+), 1 deletion(-) create mode 100755 extras/benchmarks/compareGeneralizationRuns.py create mode 100755 extras/benchmarks/makeFastqSubset.py create mode 100755 extras/benchmarks/runGeneralizationMode.sh create mode 100755 extras/benchmarks/runGeneralizationPairs.py create mode 100755 extras/tests/scripts/testGeneralizationHarness.sh diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index e3474fa5..95118842 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -41,13 +41,18 @@ jobs: bash -n extras/benchmarks/preparePublicAlignmentFixture.sh bash -n extras/benchmarks/runAlignmentA00.sh python3 -m py_compile \ + extras/benchmarks/makeFastqSubset.py \ extras/benchmarks/makePairedFastqSubset.py \ extras/benchmarks/quietSystemGate.py \ extras/benchmarks/summarizeAlignmentA00.py \ extras/benchmarks/compareAlignmentRuns.py \ + extras/benchmarks/compareGeneralizationRuns.py \ extras/benchmarks/evaluateAlignmentNoninferiority.py \ - extras/benchmarks/runAlignmentPairs.py + extras/benchmarks/runAlignmentPairs.py \ + extras/benchmarks/runGeneralizationPairs.py + bash -n extras/benchmarks/runGeneralizationMode.sh extras/tests/scripts/testBenchmarkHarness.sh + extras/tests/scripts/testGeneralizationHarness.sh - name: Validate successor metadata run: | diff --git a/extras/benchmarks/README.md b/extras/benchmarks/README.md index c2e1666d..e4ffd378 100644 --- a/extras/benchmarks/README.md +++ b/extras/benchmarks/README.md @@ -107,3 +107,38 @@ enter timing summaries. Large raw evidence belongs outside Git under `benchmarks/labs///`. Copy only anonymous, reviewed summaries into the architecture evidence ledger. + +## Cross-Workload Generalization + +Use the generalization runner for STAR modes whose input or output contract +does not fit the paired A00 harness: + +```bash +extras/benchmarks/runGeneralizationPairs.py \ + --mode single-mapping \ + --baseline-bin /local/bin/STAR-upstream \ + --candidate-bin /local/bin/STAR-blackstar \ + --genome-dir /local/index \ + --read1 /local/reads/single.fastq \ + --threads 96 --pairs 3 --output /local/evidence/single +``` + +Supported modes are paired and single-end mapping, two-pass mapping, +`BySJout`, chimeric detection, coordinate-sorted BAM, transcriptome BAM, +STARsolo, and STARlong. The driver uses seeded order-balanced pairs and +requires exact mode-specific outputs. Its acceptance test is a two-percent +wall-time noninferiority margin, no more than five-percent median RSS growth, +bounded replicate variability, and complete correctness. A separate +`median_gain_at_least_2_percent` identifies point estimates above the project's +practical speedup threshold. The stricter `superiority_2_percent` field is true +only when the lower bound of the paired bootstrap interval also clears two +percent. Noninferiority must not be described as a speed improvement. + +`makeFastqSubset.py` creates a validated, deterministic first-N subset for +single-end or long-read FASTQ input. `makePairedFastqSubset.py` additionally +checks mate names and should be used for paired or STARsolo fixtures. + +For 10x v3 STARsolo data, pass the cDNA read as `--read1`, the barcode/UMI read +as `--read2`, and export `STARSOLO_WHITELIST`. The runner pins the v3 layout to +a 16-base cell barcode followed by a 12-base UMI. For STARlong, pass the +STARlong binaries, select `--mode starlong`, and omit `--read2`. diff --git a/extras/benchmarks/compareAlignmentRuns.py b/extras/benchmarks/compareAlignmentRuns.py index 10b9a290..6bee2c7e 100755 --- a/extras/benchmarks/compareAlignmentRuns.py +++ b/extras/benchmarks/compareAlignmentRuns.py @@ -67,12 +67,28 @@ def canonical_sam_digest(path: Path) -> str: return hashlib.sha256(payload.encode()).hexdigest() +def canonical_bam_header(path: Path, samtools: str) -> bytes: + completed = subprocess.run( + [samtools, "view", "-H", str(path)], + check=True, + stdout=subprocess.PIPE, + ) + headers = [] + for line in completed.stdout.decode("utf-8", errors="strict").splitlines(): + if line.startswith("@PG") or line.startswith("@CO\tuser command line:"): + continue + headers.append(line) + return ("\n".join(headers) + "\n").encode() + + def canonical_bam_digest(path: Path, temp_root: Path) -> str: samtools = shutil.which("samtools") sort_bin = shutil.which("sort") if not samtools or not sort_bin: raise RuntimeError("samtools and GNU sort are required for canonical BAM comparison") value = hashlib.sha256() + value.update(canonical_bam_header(path, samtools)) + value.update(b"\0alignment-records\0") env = dict(os.environ, LC_ALL="C") view = subprocess.Popen([samtools, "view", str(path)], stdout=subprocess.PIPE) sorter = subprocess.Popen( diff --git a/extras/benchmarks/compareGeneralizationRuns.py b/extras/benchmarks/compareGeneralizationRuns.py new file mode 100755 index 00000000..b1d06783 --- /dev/null +++ b/extras/benchmarks/compareGeneralizationRuns.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +"""Compare timing-independent outputs from a BlackSTAR generalization pair.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +from pathlib import Path +import tempfile + +from compareAlignmentRuns import ( + canonical_bam_digest, + canonical_sam_digest, + normalized_text_digest, + parse_log, +) + + +MODES = { + "paired-mapping", + "single-mapping", + "two-pass", + "bysjout", + "chimeric", + "sorted-bam", + "transcriptome-bam", + "starsolo", + "starlong", +} + + +def digest(path: Path) -> str: + value = hashlib.sha256() + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + value.update(block) + return value.hexdigest() + + +def add_presence_digest( + checks: list[dict[str, object]], + name: str, + left: Path, + right: Path, + digest_function, +) -> None: + left_exists = left.is_file() + right_exists = right.is_file() + check: dict[str, object] = { + "name": name, + "passed": left_exists and right_exists, + } + if left_exists and right_exists: + left_digest = digest_function(left) + right_digest = digest_function(right) + check.update( + passed=left_digest == right_digest, + baseline_sha256=left_digest, + candidate_sha256=right_digest, + ) + checks.append(check) + + +def add_optional_text( + checks: list[dict[str, object]], + name: str, + left: Path, + right: Path, + sort_lines: bool, +) -> None: + if not left.exists() and not right.exists(): + return + add_presence_digest( + checks, + name, + left, + right, + lambda path: normalized_text_digest(path, sort_lines), + ) + + +def compare_solo_tree( + checks: list[dict[str, object]], baseline: Path, candidate: Path +) -> None: + baseline_root = baseline / "star.Solo.out" + candidate_root = candidate / "star.Solo.out" + baseline_files = ( + sorted( + path.relative_to(baseline_root) + for path in baseline_root.rglob("*") + if path.is_file() + ) + if baseline_root.is_dir() + else [] + ) + candidate_files = ( + sorted( + path.relative_to(candidate_root) + for path in candidate_root.rglob("*") + if path.is_file() + ) + if candidate_root.is_dir() + else [] + ) + checks.append( + { + "name": "STARsolo output inventory", + "passed": bool(baseline_files) and baseline_files == candidate_files, + "baseline_files": [str(path) for path in baseline_files], + "candidate_files": [str(path) for path in candidate_files], + } + ) + if baseline_files != candidate_files: + return + for relative in baseline_files: + add_presence_digest( + checks, + f"STARsolo {relative}", + baseline_root / relative, + candidate_root / relative, + digest, + ) + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("mode", choices=sorted(MODES)) + parser.add_argument("baseline", type=Path) + parser.add_argument("candidate", type=Path) + parser.add_argument("--temp-dir", type=Path) + parser.add_argument("--output", type=Path) + args = parser.parse_args() + + baseline = args.baseline.resolve() + candidate = args.candidate.resolve() + checks: list[dict[str, object]] = [] + + left_log = baseline / "star.Log.final.out" + right_log = candidate / "star.Log.final.out" + if left_log.is_file() and right_log.is_file(): + checks.append( + { + "name": "timing-independent Log.final.out fields", + "passed": parse_log(left_log) == parse_log(right_log), + } + ) + else: + checks.append( + { + "name": "Log.final.out presence", + "passed": False, + } + ) + + add_optional_text( + checks, + "star.SJ.out.tab", + baseline / "star.SJ.out.tab", + candidate / "star.SJ.out.tab", + True, + ) + add_optional_text( + checks, + "star.ReadsPerGene.out.tab", + baseline / "star.ReadsPerGene.out.tab", + candidate / "star.ReadsPerGene.out.tab", + False, + ) + + left_main_sam = baseline / "star.Aligned.out.sam" + right_main_sam = candidate / "star.Aligned.out.sam" + if left_main_sam.exists() or right_main_sam.exists(): + add_presence_digest( + checks, + "canonical genomic SAM records", + left_main_sam, + right_main_sam, + canonical_sam_digest, + ) + + if args.mode == "chimeric": + add_presence_digest( + checks, + "canonical chimeric SAM records", + baseline / "star.Chimeric.out.sam", + candidate / "star.Chimeric.out.sam", + canonical_sam_digest, + ) + add_presence_digest( + checks, + "chimeric junctions", + baseline / "star.Chimeric.out.junction", + candidate / "star.Chimeric.out.junction", + lambda path: normalized_text_digest(path, True), + ) + + bam_names: list[str] = [] + if args.mode == "sorted-bam": + bam_names.append("star.Aligned.sortedByCoord.out.bam") + elif args.mode == "transcriptome-bam": + bam_names.extend( + ( + "star.Aligned.out.bam", + "star.Aligned.toTranscriptome.out.bam", + ) + ) + if bam_names: + parent = args.temp_dir or Path(tempfile.gettempdir()) + with tempfile.TemporaryDirectory( + prefix="blackstar-generalization-bam-", dir=parent + ) as raw: + temp_root = Path(raw) + for name in bam_names: + add_presence_digest( + checks, + f"canonical {name} records", + baseline / name, + candidate / name, + lambda path, root=temp_root: canonical_bam_digest(path, root), + ) + + if args.mode == "starsolo": + compare_solo_tree(checks, baseline, candidate) + + result = { + "schema": "blackstar-generalization-comparison-v1", + "mode": args.mode, + "baseline": str(baseline), + "candidate": str(candidate), + "passed": bool(checks) and all(bool(check["passed"]) for check in checks), + "checks": checks, + } + output = json.dumps(result, indent=2, sort_keys=True) + "\n" + if args.output: + args.output.write_text(output, encoding="utf-8") + else: + print(output, end="") + return 0 if result["passed"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/extras/benchmarks/makeFastqSubset.py b/extras/benchmarks/makeFastqSubset.py new file mode 100755 index 00000000..3a74a965 --- /dev/null +++ b/extras/benchmarks/makeFastqSubset.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Create a deterministic, validated first-N subset of one FASTQ file.""" + +from __future__ import annotations + +import argparse +from contextlib import contextmanager +import gzip +import hashlib +import os +from pathlib import Path +import tempfile +from typing import BinaryIO, Iterator + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--input", type=Path, required=True) + parser.add_argument("--records", type=int, required=True) + parser.add_argument("--output", type=Path, required=True) + return parser.parse_args() + + +def read_record(handle: BinaryIO, index: int) -> list[bytes] | None: + lines = [handle.readline() for _ in range(4)] + if lines[0] == b"": + if any(lines[1:]): + raise ValueError(f"truncated record after EOF at record {index}") + return None + if any(line == b"" for line in lines): + raise ValueError(f"truncated FASTQ record {index}") + if not lines[0].startswith(b"@") or not lines[2].startswith(b"+"): + raise ValueError(f"malformed FASTQ record {index}") + sequence = lines[1].rstrip(b"\r\n") + quality = lines[3].rstrip(b"\r\n") + if len(sequence) != len(quality): + raise ValueError(f"sequence/quality length mismatch at record {index}") + return lines + + +def sha256(path: Path) -> str: + value = hashlib.sha256() + with path.open("rb") as handle: + for block in iter(lambda: handle.read(1024 * 1024), b""): + value.update(block) + return value.hexdigest() + + +@contextmanager +def open_input(path: Path) -> Iterator[BinaryIO]: + opener = gzip.open if path.suffix == ".gz" else open + with opener(path, "rb") as handle: + yield handle + + +def open_output(path: Path) -> tuple[Path, BinaryIO]: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, raw = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + os.close(descriptor) + temporary = Path(raw) + binary = temporary.open("wb") + if path.suffix == ".gz": + return temporary, gzip.GzipFile( + filename="", mode="wb", fileobj=binary, mtime=0 + ) + return temporary, binary + + +def main() -> int: + args = parse_args() + if args.records <= 0: + raise SystemExit("--records must be positive") + if not args.input.is_file(): + raise SystemExit(f"input file is absent: {args.input}") + if args.input.resolve() == args.output.resolve(): + raise SystemExit("input and output must be distinct") + + temporary, output = open_output(args.output) + completed = False + try: + with open_input(args.input) as source: + for index in range(1, args.records + 1): + record = read_record(source, index) + if record is None: + raise ValueError(f"input ended before requested record {index}") + output.writelines(record) + completed = True + finally: + output.close() + if not completed: + temporary.unlink(missing_ok=True) + + os.replace(temporary, args.output) + print( + f"wrote {args.records} records; output_sha256={sha256(args.output)}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/extras/benchmarks/runGeneralizationMode.sh b/extras/benchmarks/runGeneralizationMode.sh new file mode 100755 index 00000000..c81aa23a --- /dev/null +++ b/extras/benchmarks/runGeneralizationMode.sh @@ -0,0 +1,298 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +usage: runGeneralizationMode.sh MODE STAR_BIN GENOME_DIR READ1 READ2_OR_NONE OUT_DIR + +MODE is one of: + paired-mapping + single-mapping + two-pass + bysjout + chimeric + sorted-bam + transcriptome-bam + starsolo + starlong + +Environment: + THREADS=96 + GENOME_LOAD_MODE=NoSharedMemory + READ_FILES_COMMAND=auto + BAM_SORT_RAM=30000000000 + STAR_EXTRA_ARGS="" + STARSOLO_WHITELIST=/path/to/3M-february-2018.txt + +For starsolo, READ1 is the cDNA read and READ2 is the barcode/UMI read. +Use READ2_OR_NONE=none for single-mapping and starlong. +EOF + exit 2 +} + +[[ $# -eq 6 ]] || usage +mode="$1" +star_bin="$(realpath "$2")" +genome_dir="$(realpath "$3")" +read1="$(realpath "$4")" +read2_arg="$5" +out_dir="$6" +threads="${THREADS:-96}" +genome_load_mode="${GENOME_LOAD_MODE:-NoSharedMemory}" +read_files_command="${READ_FILES_COMMAND:-auto}" +allow_omp_thread_binding="${ALLOW_OMP_THREAD_BINDING:-0}" + +case "${mode}" in + paired-mapping|single-mapping|two-pass|bysjout|chimeric|sorted-bam|transcriptome-bam|starsolo|starlong) ;; + *) usage ;; +esac + +case "${mode}" in + single-mapping|starlong) + [[ "${read2_arg}" == "none" ]] || { + printf '%s requires READ2_OR_NONE=none\n' "${mode}" >&2 + exit 2 + } + read2="none" + ;; + *) + [[ "${read2_arg}" != "none" ]] || { + printf '%s requires two input files\n' "${mode}" >&2 + exit 2 + } + read2="$(realpath "${read2_arg}")" + ;; +esac + +[[ -x "${star_bin}" ]] || { + printf 'STAR binary is not executable: %s\n' "${star_bin}" >&2 + exit 2 +} +[[ -f "${genome_dir}/Genome" && -f "${genome_dir}/SA" && + -f "${genome_dir}/SAindex" && -f "${genome_dir}/genomeParameters.txt" ]] || { + printf 'incomplete genome directory: %s\n' "${genome_dir}" >&2 + exit 2 +} +[[ -f "${read1}" ]] || { + printf 'input file is absent: %s\n' "${read1}" >&2 + exit 2 +} +if [[ "${read2}" != "none" && ! -f "${read2}" ]]; then + printf 'input file is absent: %s\n' "${read2}" >&2 + exit 2 +fi +[[ "${threads}" =~ ^[1-9][0-9]*$ ]] || { + printf 'THREADS must be positive\n' >&2 + exit 2 +} +[[ ! -e "${out_dir}" ]] || { + printf 'output path already exists: %s\n' "${out_dir}" >&2 + exit 2 +} +[[ "${allow_omp_thread_binding}" == "0" || + "${allow_omp_thread_binding}" == "1" ]] || { + printf 'ALLOW_OMP_THREAD_BINDING must be 0 or 1\n' >&2 + exit 2 +} + +omp_proc_bind_normalized="${OMP_PROC_BIND:-}" +omp_proc_bind_normalized="${omp_proc_bind_normalized,,}" +if [[ "${allow_omp_thread_binding}" != "1" ]] && + { [[ -n "${OMP_PLACES:-}" ]] || + [[ -n "${omp_proc_bind_normalized}" && + "${omp_proc_bind_normalized}" != "false" ]]; }; then + printf '%s\n' \ + 'OpenMP processor binding is unsafe for alignment comparisons.' \ + 'Unset OMP_PROC_BIND and OMP_PLACES, or explicitly allow a controlled affinity test.' >&2 + exit 2 +fi + +solo_whitelist="none" +if [[ "${mode}" == "starsolo" ]]; then + [[ -n "${STARSOLO_WHITELIST:-}" && + -f "${STARSOLO_WHITELIST}" ]] || { + printf 'STARSOLO_WHITELIST must name an existing file\n' >&2 + exit 2 + } + solo_whitelist="$(realpath "${STARSOLO_WHITELIST}")" +fi + +case "${genome_load_mode}" in + NoSharedMemory|LoadAndKeep|LoadAndRemove) ;; + *) + printf 'unsupported GENOME_LOAD_MODE: %s\n' "${genome_load_mode}" >&2 + exit 2 + ;; +esac + +inputs=("${read1}") +if [[ "${read2}" != "none" ]]; then + inputs+=("${read2}") +fi + +case "${read_files_command}" in + auto) + all_gzip=1 + any_gzip=0 + for input in "${inputs[@]}"; do + if [[ "${input}" == *.gz ]]; then + any_gzip=1 + else + all_gzip=0 + fi + done + if (( all_gzip )); then + read_command=(zcat) + elif (( any_gzip )); then + printf 'all inputs must use the same compression mode\n' >&2 + exit 2 + else + read_command=() + fi + ;; + None|none) + read_command=() + ;; + *) + read -r -a read_command <<< "${read_files_command}" + ;; +esac + +mkdir -p "${out_dir}" +export OMP_DYNAMIC=FALSE + +command=( + "${star_bin}" + --runMode alignReads + --runThreadN "${threads}" + --genomeDir "${genome_dir}" + --genomeLoad "${genome_load_mode}" + --readFilesIn "${inputs[@]}" + --outFileNamePrefix "${out_dir}/star." +) +if (( ${#read_command[@]} > 0 )); then + command+=(--readFilesCommand "${read_command[@]}") +fi + +case "${mode}" in + paired-mapping|single-mapping) + command+=(--outSAMtype None --quantMode GeneCounts) + ;; + two-pass) + command+=( + --twopassMode Basic + --outSAMtype None + --quantMode GeneCounts + ) + ;; + bysjout) + command+=( + --outFilterType BySJout + --outSAMtype None + --quantMode GeneCounts + ) + ;; + chimeric) + command+=( + --outSAMtype None + --quantMode GeneCounts + --chimSegmentMin 30 + --chimJunctionOverhangMin 20 + --chimScoreMin 30 + --chimScoreDropMax 100 + --chimScoreSeparation 0 + --chimFilter None + --chimOutType Junctions SeparateSAMold + ) + ;; + sorted-bam) + command+=( + --outSAMtype BAM SortedByCoordinate + --quantMode GeneCounts + --limitBAMsortRAM "${BAM_SORT_RAM:-30000000000}" + ) + ;; + transcriptome-bam) + command+=( + --outSAMtype BAM Unsorted + --quantMode TranscriptomeSAM GeneCounts + ) + ;; + starsolo) + command+=( + --soloType CB_UMI_Simple + --soloCBstart 1 + --soloCBlen 16 + --soloUMIstart 17 + --soloUMIlen 12 + --soloCBwhitelist "${solo_whitelist}" + --soloCBmatchWLtype 1MM_multi_Nbase_pseudocounts + --soloUMIdedup 1MM_CR + --soloCellFilter None + --soloStrand Forward + --soloFeatures Gene + --outSAMtype None + ) + ;; + starlong) + command+=( + --outSAMtype SAM + --outSAMattributes Standard + --outSJtype None + ) + ;; +esac + +if [[ -n "${STAR_EXTRA_ARGS:-}" ]]; then + read -r -a extra_args <<< "${STAR_EXTRA_ARGS}" + command+=("${extra_args[@]}") +fi + +{ + printf 'field\tvalue\n' + printf 'schema\tblackstar-generalization-run-v1\n' + printf 'mode\t%s\n' "${mode}" + printf 'start_utc\t%s\n' "$(date --utc --iso-8601=seconds)" + printf 'host\t%s\n' "$(hostname)" + printf 'threads\t%s\n' "${threads}" + printf 'star_version\t%s\n' "$("${star_bin}" --version)" + printf 'star_sha256\t%s\n' "$(sha256sum "${star_bin}" | awk '{print $1}')" + printf 'genome_dir\t%s\n' "${genome_dir}" + printf 'genome_parameters_sha256\t%s\n' \ + "$(sha256sum "${genome_dir}/genomeParameters.txt" | awk '{print $1}')" + printf 'read1\t%s\n' "${read1}" + printf 'read1_bytes\t%s\n' "$(stat --format='%s' "${read1}")" + printf 'read1_sha256\t%s\n' "$(sha256sum "${read1}" | awk '{print $1}')" + printf 'read2\t%s\n' "${read2}" + if [[ "${read2}" != "none" ]]; then + printf 'read2_bytes\t%s\n' "$(stat --format='%s' "${read2}")" + printf 'read2_sha256\t%s\n' "$(sha256sum "${read2}" | awk '{print $1}')" + fi + printf 'read_files_command\t%s\n' "${read_command[*]:-None}" + printf 'starsolo_whitelist\t%s\n' "${solo_whitelist}" + if [[ "${solo_whitelist}" != "none" ]]; then + printf 'starsolo_whitelist_sha256\t%s\n' \ + "$(sha256sum "${solo_whitelist}" | awk '{print $1}')" + fi + printf 'genome_load_mode\t%s\n' "${genome_load_mode}" + printf 'omp_dynamic\t%s\n' "${OMP_DYNAMIC}" + printf 'omp_proc_bind\t%s\n' "${OMP_PROC_BIND:-unset}" + printf 'omp_places\t%s\n' "${OMP_PLACES:-unset}" + printf 'cpu_allowed_list\t%s\n' \ + "$(awk '/Cpus_allowed_list:/ {print $2}' /proc/self/status)" + printf 'kernel\t%s\n' "$(uname -sr)" +} > "${out_dir}/provenance.tsv" + +printf '%q ' "${command[@]}" > "${out_dir}/command.sh" +printf '\n' >> "${out_dir}/command.sh" +lscpu > "${out_dir}/lscpu.txt" +numactl --hardware > "${out_dir}/numa.txt" 2>&1 || true + +/usr/bin/time -v -o "${out_dir}/time.txt" \ + "${command[@]}" > "${out_dir}/stdout.log" 2> "${out_dir}/stderr.log" + +printf 'finish_utc\t%s\n' "$(date --utc --iso-8601=seconds)" \ + >> "${out_dir}/provenance.tsv" +find "${out_dir}" -maxdepth 1 -type f -printf '%f\n' | + LC_ALL=C sort > "${out_dir}/files.txt" +printf 'generalization run complete: %s\n' "${out_dir}" diff --git a/extras/benchmarks/runGeneralizationPairs.py b/extras/benchmarks/runGeneralizationPairs.py new file mode 100755 index 00000000..58b0e854 --- /dev/null +++ b/extras/benchmarks/runGeneralizationPairs.py @@ -0,0 +1,442 @@ +#!/usr/bin/env python3 +"""Run order-balanced cross-workload STAR/BlackSTAR noninferiority pairs.""" + +from __future__ import annotations + +import argparse +import csv +import json +import os +from pathlib import Path +import random +import statistics +import subprocess +import sys +import time + +from runAlignmentPairs import ( + bootstrap_median_ci, + coefficient_of_variation, + elapsed_seconds, + make_orders, + max_rss_kib, + sha256, + utc_now, + validate_affinity_environment, + write_schedule, +) + + +REPO_ROOT = Path(__file__).resolve().parents[2] +DEFAULT_RUNNER = REPO_ROOT / "extras/benchmarks/runGeneralizationMode.sh" +COMPARATOR = REPO_ROOT / "extras/benchmarks/compareGeneralizationRuns.py" +QUIET_GATE = REPO_ROOT / "extras/benchmarks/quietSystemGate.py" +PAIRED_MODES = { + "paired-mapping", + "two-pass", + "bysjout", + "chimeric", + "sorted-bam", + "transcriptome-bam", + "starsolo", +} +SINGLE_MODES = {"single-mapping", "starlong"} +MODES = PAIRED_MODES | SINGLE_MODES + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--mode", choices=sorted(MODES), required=True) + parser.add_argument("--baseline-bin", type=Path, required=True) + parser.add_argument("--candidate-bin", type=Path, required=True) + parser.add_argument("--genome-dir", type=Path, required=True) + parser.add_argument("--read1", type=Path, required=True) + parser.add_argument("--read2", type=Path) + parser.add_argument("--warmup-read1", type=Path) + parser.add_argument("--warmup-read2", type=Path) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--threads", type=int, default=96) + parser.add_argument("--pairs", type=int, default=3) + parser.add_argument("--seed", type=int, default=20260725) + parser.add_argument("--margin-percent", type=float, default=2.0) + parser.add_argument("--max-rss-increase-percent", type=float, default=5.0) + parser.add_argument("--max-cv-percent", type=float, default=5.0) + parser.add_argument("--quiet-duration", type=float, default=300.0) + parser.add_argument("--quiet-interval", type=float, default=5.0) + parser.add_argument("--settle-seconds", type=float, default=5.0) + parser.add_argument("--runner", type=Path, default=DEFAULT_RUNNER) + parser.add_argument("--skip-quiet-gate", action="store_true") + return parser.parse_args() + + +def validate(args: argparse.Namespace) -> None: + validate_affinity_environment() + for path in ( + args.baseline_bin, + args.candidate_bin, + args.read1, + args.runner, + ): + if not path.is_file(): + raise SystemExit(f"required file is absent: {path}") + for path in (args.baseline_bin, args.candidate_bin, args.runner): + if not os.access(path, os.X_OK): + raise SystemExit(f"required executable is not executable: {path}") + for name in ("Genome", "SA", "SAindex", "genomeParameters.txt"): + if not (args.genome_dir / name).is_file(): + raise SystemExit(f"incomplete genome directory: missing {name}") + if args.mode in PAIRED_MODES and args.read2 is None: + raise SystemExit(f"{args.mode} requires --read2") + if args.mode in SINGLE_MODES and args.read2 is not None: + raise SystemExit(f"{args.mode} does not accept --read2") + if args.read2 is not None and not args.read2.is_file(): + raise SystemExit(f"required file is absent: {args.read2}") + if args.warmup_read1 is None and args.warmup_read2 is not None: + raise SystemExit("--warmup-read2 requires --warmup-read1") + if args.warmup_read1 is not None and not args.warmup_read1.is_file(): + raise SystemExit(f"required file is absent: {args.warmup_read1}") + if args.mode in PAIRED_MODES: + if (args.warmup_read1 is None) != (args.warmup_read2 is None): + raise SystemExit("paired-mode warmups require both warmup reads") + elif args.warmup_read2 is not None: + raise SystemExit("single-mode warmups do not accept --warmup-read2") + if args.warmup_read2 is not None and not args.warmup_read2.is_file(): + raise SystemExit(f"required file is absent: {args.warmup_read2}") + if args.mode == "starsolo": + whitelist = os.environ.get("STARSOLO_WHITELIST", "") + if not whitelist or not Path(whitelist).is_file(): + raise SystemExit( + "STARSOLO_WHITELIST must name an existing file for starsolo" + ) + if args.output.exists(): + raise SystemExit(f"output path already exists: {args.output}") + if args.threads < 1 or args.pairs < 3: + raise SystemExit("threads must be positive and at least three pairs are required") + if args.margin_percent <= 0 or args.max_rss_increase_percent < 0: + raise SystemExit("acceptance margins are invalid") + if args.max_cv_percent <= 0: + raise SystemExit("--max-cv-percent must be positive") + if args.quiet_duration <= 0 or args.quiet_interval <= 0: + raise SystemExit("quiet-system timing must be positive") + if args.settle_seconds < 0: + raise SystemExit("--settle-seconds cannot be negative") + + +def input_value(path: Path | None) -> str: + return str(path.resolve()) if path is not None else "none" + + +def input_digest(path: Path | None) -> str: + return sha256(path) if path is not None else "none" + + +def write_contract( + args: argparse.Namespace, orders: list[str], warmup_order: str | None +) -> None: + whitelist = ( + Path(os.environ["STARSOLO_WHITELIST"]).resolve() + if args.mode == "starsolo" + else None + ) + contract = { + "schema": "blackstar-generalization-pairs-v1", + "created_utc": utc_now(), + "mode": args.mode, + "threads": args.threads, + "pairs": args.pairs, + "orders": orders, + "seed": args.seed, + "margin_percent": args.margin_percent, + "max_rss_increase_percent": args.max_rss_increase_percent, + "max_cv_percent": args.max_cv_percent, + "quiet_duration": args.quiet_duration, + "quiet_gate_skipped": args.skip_quiet_gate, + "settle_seconds": args.settle_seconds, + "baseline_bin": str(args.baseline_bin.resolve()), + "baseline_sha256": sha256(args.baseline_bin), + "candidate_bin": str(args.candidate_bin.resolve()), + "candidate_sha256": sha256(args.candidate_bin), + "genome_dir": str(args.genome_dir.resolve()), + "genome_parameters_sha256": sha256( + args.genome_dir / "genomeParameters.txt" + ), + "genome_core_sizes": { + name: (args.genome_dir / name).stat().st_size + for name in ("Genome", "SA", "SAindex") + }, + "read1": input_value(args.read1), + "read1_sha256": input_digest(args.read1), + "read2": input_value(args.read2), + "read2_sha256": input_digest(args.read2), + "warmup_order": warmup_order or "none", + "warmup_read1": input_value(args.warmup_read1), + "warmup_read1_sha256": input_digest(args.warmup_read1), + "warmup_read2": input_value(args.warmup_read2), + "warmup_read2_sha256": input_digest(args.warmup_read2), + "starsolo_whitelist": input_value(whitelist), + "starsolo_whitelist_sha256": input_digest(whitelist), + "tools": { + "pair_driver_sha256": sha256(Path(__file__)), + "runner_sha256": sha256(args.runner), + "quiet_gate_sha256": sha256(QUIET_GATE), + "comparator_sha256": sha256(COMPARATOR), + }, + "environment": { + name: os.environ.get(name, "unset") + for name in ( + "ALLOW_OMP_THREAD_BINDING", + "BAM_SORT_RAM", + "GENOME_LOAD_MODE", + "OMP_DYNAMIC", + "OMP_PLACES", + "OMP_PROC_BIND", + "READ_FILES_COMMAND", + "STARSOLO_WHITELIST", + "STAR_EXTRA_ARGS", + ) + }, + } + (args.output / "contract.json").write_text( + json.dumps(contract, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + + +def run_one( + args: argparse.Namespace, role: str, read1: Path, read2: Path | None, out: Path +) -> None: + binary = args.baseline_bin if role == "baseline" else args.candidate_bin + environment = dict(os.environ) + environment.update(THREADS=str(args.threads)) + subprocess.run( + [ + str(args.runner), + args.mode, + str(binary), + str(args.genome_dir), + str(read1), + str(read2) if read2 is not None else "none", + str(out), + ], + env=environment, + check=True, + ) + + +def compare_pair( + args: argparse.Namespace, baseline: Path, candidate: Path, output: Path +) -> bool: + status = subprocess.run( + [ + sys.executable, + str(COMPARATOR), + args.mode, + str(baseline), + str(candidate), + "--temp-dir", + str(args.output), + "--output", + str(output), + ] + ).returncode + result = json.loads(output.read_text(encoding="utf-8")) + return status == 0 and bool(result["passed"]) + + +def run_quiet_gate(args: argparse.Namespace) -> None: + if args.skip_quiet_gate: + return + subprocess.run( + [ + sys.executable, + str(QUIET_GATE), + "--duration", + str(args.quiet_duration), + "--interval", + str(args.quiet_interval), + "--path", + str(args.output), + "--output", + str(args.output / "quiet-system.tsv"), + ], + check=True, + ) + + +def main() -> int: + args = parse_args() + validate(args) + args.output.mkdir(parents=True) + orders = make_orders(args.pairs, args.seed) + warmup_order = ( + random.Random(args.seed ^ 0xC012).choice(["AB", "BA"]) + if args.warmup_read1 is not None + else None + ) + write_contract(args, orders, warmup_order) + + if warmup_order is not None: + roles = ( + ("baseline", "candidate") + if warmup_order == "AB" + else ("candidate", "baseline") + ) + warmups: dict[str, Path] = {} + assert args.warmup_read1 is not None + for position, role in enumerate(roles, 1): + output = args.output / f"warmup-{position}-{role}" + run_one( + args, + role, + args.warmup_read1, + args.warmup_read2, + output, + ) + warmups[role] = output + if not compare_pair( + args, + warmups["baseline"], + warmups["candidate"], + args.output / "warmup-comparison.json", + ): + raise RuntimeError("warmup correctness comparison failed") + + run_quiet_gate(args) + schedule: list[dict[str, str]] = [] + for pair, order in enumerate(orders, 1): + roles = ( + ("baseline", "candidate") if order == "AB" else ("candidate", "baseline") + ) + for position, role in enumerate(roles, 1): + schedule.append( + { + "pair": str(pair), + "order": order, + "position": str(position), + "role": role, + "run_dir": f"pair-{pair:02d}-{position}-{role}", + "status": "pending", + "started_utc": "", + "finished_utc": "", + } + ) + write_schedule(args.output / "schedule.tsv", schedule) + + for index, row in enumerate(schedule): + row["status"] = "running" + row["started_utc"] = utc_now() + write_schedule(args.output / "schedule.tsv", schedule) + try: + run_one( + args, + row["role"], + args.read1, + args.read2, + args.output / row["run_dir"], + ) + except BaseException: + row["status"] = "failed" + row["finished_utc"] = utc_now() + write_schedule(args.output / "schedule.tsv", schedule) + raise + row["status"] = "complete" + row["finished_utc"] = utc_now() + write_schedule(args.output / "schedule.tsv", schedule) + if index + 1 < len(schedule) and args.settle_seconds: + time.sleep(args.settle_seconds) + + baseline_times: list[float] = [] + candidate_times: list[float] = [] + baseline_rss: list[int] = [] + candidate_rss: list[int] = [] + improvements: list[float] = [] + summary_rows: list[dict[str, object]] = [] + correctness: list[bool] = [] + for pair in range(1, args.pairs + 1): + pair_rows = [row for row in schedule if int(row["pair"]) == pair] + runs = { + row["role"]: args.output / row["run_dir"] for row in pair_rows + } + passed = compare_pair( + args, + runs["baseline"], + runs["candidate"], + args.output / f"pair-{pair:02d}-comparison.json", + ) + baseline_wall = elapsed_seconds(runs["baseline"] / "time.txt") + candidate_wall = elapsed_seconds(runs["candidate"] / "time.txt") + baseline_peak = max_rss_kib(runs["baseline"] / "time.txt") + candidate_peak = max_rss_kib(runs["candidate"] / "time.txt") + improvement = 100.0 * (baseline_wall - candidate_wall) / baseline_wall + baseline_times.append(baseline_wall) + candidate_times.append(candidate_wall) + baseline_rss.append(baseline_peak) + candidate_rss.append(candidate_peak) + improvements.append(improvement) + correctness.append(passed) + summary_rows.append( + { + "pair": pair, + "order": pair_rows[0]["order"], + "baseline_wall_seconds": baseline_wall, + "candidate_wall_seconds": candidate_wall, + "improvement_percent": improvement, + "baseline_max_rss_kib": baseline_peak, + "candidate_max_rss_kib": candidate_peak, + "correctness_passed": passed, + } + ) + + ci_low, ci_high = bootstrap_median_ci(improvements, args.seed ^ 0x6E) + baseline_cv = coefficient_of_variation(baseline_times) + candidate_cv = coefficient_of_variation(candidate_times) + median_baseline_rss = statistics.median(baseline_rss) + median_candidate_rss = statistics.median(candidate_rss) + rss_increase = ( + 100.0 + * (median_candidate_rss - median_baseline_rss) + / median_baseline_rss + ) + gates = { + "correctness": all(correctness), + "pair_count": len(summary_rows) >= 3, + "variability": max(baseline_cv, candidate_cv) <= args.max_cv_percent, + "wall_time_noninferiority": ci_low >= -args.margin_percent, + "rss": rss_increase <= args.max_rss_increase_percent, + } + result = { + "schema": "blackstar-generalization-result-v1", + "mode": args.mode, + "accepted": all(gates.values()), + "gates": gates, + "pair_count": len(summary_rows), + "margin_percent": args.margin_percent, + "median_baseline_wall_seconds": statistics.median(baseline_times), + "median_candidate_wall_seconds": statistics.median(candidate_times), + "median_improvement_percent": statistics.median(improvements), + "bootstrap_95_ci_percent": [ci_low, ci_high], + "baseline_cv_percent": baseline_cv, + "candidate_cv_percent": candidate_cv, + "median_rss_increase_percent": rss_increase, + "median_gain_at_least_2_percent": statistics.median(improvements) >= 2.0, + "superiority_2_percent": ci_low >= 2.0, + } + (args.output / "result.json").write_text( + json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8" + ) + with (args.output / "pairs.tsv").open( + "w", encoding="utf-8", newline="" + ) as handle: + writer = csv.DictWriter( + handle, + fieldnames=list(summary_rows[0]), + delimiter="\t", + lineterminator="\n", + ) + writer.writeheader() + writer.writerows(summary_rows) + print(json.dumps(result, indent=2, sort_keys=True)) + return 0 if result["accepted"] else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/extras/tests/scripts/testBenchmarkHarness.sh b/extras/tests/scripts/testBenchmarkHarness.sh index c51c8066..075efddd 100755 --- a/extras/tests/scripts/testBenchmarkHarness.sh +++ b/extras/tests/scripts/testBenchmarkHarness.sh @@ -38,6 +38,20 @@ cmp "${tmp_dir}/subset-r2-1.fastq.gz" "${tmp_dir}/subset-r2-2.fastq.gz" [[ "$(gzip -cd "${tmp_dir}/subset-r1-1.fastq.gz" | wc -l)" -eq 8 ]] [[ "$(gzip -cd "${tmp_dir}/subset-r2-1.fastq.gz" | wc -l)" -eq 8 ]] +for iteration in 1 2; do + python3 "${repo_root}/extras/benchmarks/makeFastqSubset.py" \ + --input "${tmp_dir}/read1.fastq.gz" \ + --records 2 \ + --output "${tmp_dir}/single-${iteration}.fastq.gz" +done +cmp "${tmp_dir}/single-1.fastq.gz" "${tmp_dir}/single-2.fastq.gz" +[[ "$(gzip -cd "${tmp_dir}/single-1.fastq.gz" | wc -l)" -eq 8 ]] +python3 "${repo_root}/extras/benchmarks/makeFastqSubset.py" \ + --input "${tmp_dir}/read1.fastq.gz" \ + --records 2 \ + --output "${tmp_dir}/single.fastq" +[[ "$(wc -l < "${tmp_dir}/single.fastq")" -eq 8 ]] + python3 "${repo_root}/extras/benchmarks/quietSystemGate.py" \ --duration 0.1 --interval 0.05 \ --min-idle 0 --max-iowait 100 --max-storage-util 1000 \ @@ -91,6 +105,32 @@ python3 "${repo_root}/extras/benchmarks/compareAlignmentRuns.py" \ --output "${tmp_dir}/comparison.json" jq -e '.passed == true and (.checks | length) == 4' "${tmp_dir}/comparison.json" > /dev/null +for run in bam-header-a bam-header-b; do + mkdir -p "${tmp_dir}/${run}" + cp "${tmp_dir}/baseline/star.Log.final.out" "${tmp_dir}/${run}/star.Log.final.out" +done +{ + printf '@HD\tVN:1.4\tSO:unsorted\n' + printf '@SQ\tSN:chr1\tLN:8\n' + printf 'read1\t0\tchr1\t1\t255\t8M\t*\t0\t0\tACGTACGT\tIIIIIIII\n' +} | samtools view -b -o "${tmp_dir}/bam-header-a/star.Aligned.out.bam" +{ + printf '@HD\tVN:1.4\tSO:unsorted\n' + printf '@SQ\tSN:chr1\tLN:9\n' + printf 'read1\t0\tchr1\t1\t255\t8M\t*\t0\t0\tACGTACGT\tIIIIIIII\n' +} | samtools view -b -o "${tmp_dir}/bam-header-b/star.Aligned.out.bam" +if python3 "${repo_root}/extras/benchmarks/compareAlignmentRuns.py" \ + "${tmp_dir}/bam-header-a" "${tmp_dir}/bam-header-b" \ + --canonical-bam --temp-dir "${tmp_dir}" \ + --output "${tmp_dir}/bam-header-comparison.json"; then + printf 'canonical BAM comparison ignored a semantic header difference\n' >&2 + exit 1 +fi +jq -e ' + .passed == false and + any(.checks[]; .name == "canonical BAM records" and .passed == false) +' "${tmp_dir}/bam-header-comparison.json" > /dev/null + mkdir -p "${tmp_dir}/pair-genome" for file in Genome SA SAindex genomeParameters.txt; do printf '%s\n' "${file}" > "${tmp_dir}/pair-genome/${file}" diff --git a/extras/tests/scripts/testGeneralizationHarness.sh b/extras/tests/scripts/testGeneralizationHarness.sh new file mode 100755 index 00000000..c75cc7cb --- /dev/null +++ b/extras/tests/scripts/testGeneralizationHarness.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../../.." && pwd)" +out_root="$(mktemp -d "${TMPDIR:-/tmp}/blackstar-generalization-harness.XXXXXX")" + +cleanup() { + rm -rf "${out_root}" +} +trap cleanup EXIT + +mkdir -p "${out_root}/index" +for name in Genome SA SAindex genomeParameters.txt; do + printf '%s\n' "${name}" > "${out_root}/index/${name}" +done +printf '@read1\nACGT\n+\nIIII\n' > "${out_root}/read1.fq" +printf '@read1\nTGCA\n+\nIIII\n' > "${out_root}/read2.fq" +printf 'ACGTACGTACGTACGT\n' > "${out_root}/whitelist.txt" +printf '#!/usr/bin/env bash\nexit 0\n' > "${out_root}/baseline" +printf '#!/usr/bin/env bash\nexit 0\n' > "${out_root}/candidate" +chmod +x "${out_root}/baseline" "${out_root}/candidate" + +cat > "${out_root}/fake-runner" <<'EOF_RUNNER' +#!/usr/bin/env bash +set -euo pipefail +mode="$1" +binary="$2" +out="$6" +mkdir -p "${out}" +if [[ "$(basename "${binary}")" == "baseline" ]]; then + wall="0:10.00" + rss="1000" +else + wall="0:09.00" + rss="990" +fi +cat > "${out}/time.txt" < "${out}/star.Log.final.out" <<'EOF_LOG' +Number of input reads | 1 +Uniquely mapped reads number | 1 +EOF_LOG +printf 'chrSynthetic\t1\t2\t1\t1\t1\t0\t0\t1\n' > "${out}/star.SJ.out.tab" +printf 'N_unmapped\t0\t0\t0\n' > "${out}/star.ReadsPerGene.out.tab" +printf 'mode\t%s\n' "${mode}" > "${out}/provenance.tsv" +if [[ "${mode}" == "starsolo" ]]; then + mkdir -p "${out}/star.Solo.out/Gene/raw" + printf 'cell\n' > "${out}/star.Solo.out/Gene/raw/barcodes.tsv" + printf 'gene\tgene\tGene Expression\n' \ + > "${out}/star.Solo.out/Gene/raw/features.tsv" + printf '%%%%MatrixMarket matrix coordinate integer general\n1 1 1\n1 1 1\n' \ + > "${out}/star.Solo.out/Gene/raw/matrix.mtx" +fi +EOF_RUNNER +chmod +x "${out_root}/fake-runner" + +python3 "${repo_root}/extras/benchmarks/runGeneralizationPairs.py" \ + --mode paired-mapping \ + --baseline-bin "${out_root}/baseline" \ + --candidate-bin "${out_root}/candidate" \ + --genome-dir "${out_root}/index" \ + --read1 "${out_root}/read1.fq" \ + --read2 "${out_root}/read2.fq" \ + --output "${out_root}/result" \ + --runner "${out_root}/fake-runner" \ + --threads 4 \ + --pairs 3 \ + --settle-seconds 0 \ + --skip-quiet-gate \ + > "${out_root}/driver.log" + +python3 - "${out_root}/result/result.json" <<'EOF_CHECK' +import json +import sys + +result = json.load(open(sys.argv[1], encoding="utf-8")) +assert result["accepted"] +assert result["gates"]["correctness"] +assert result["gates"]["wall_time_noninferiority"] +assert result["median_improvement_percent"] == 10.0 +assert result["median_gain_at_least_2_percent"] +assert result["superiority_2_percent"] +EOF_CHECK + +STARSOLO_WHITELIST="${out_root}/whitelist.txt" \ +python3 "${repo_root}/extras/benchmarks/runGeneralizationPairs.py" \ + --mode starsolo \ + --baseline-bin "${out_root}/baseline" \ + --candidate-bin "${out_root}/candidate" \ + --genome-dir "${out_root}/index" \ + --read1 "${out_root}/read1.fq" \ + --read2 "${out_root}/read2.fq" \ + --output "${out_root}/solo-result" \ + --runner "${out_root}/fake-runner" \ + --threads 4 \ + --pairs 3 \ + --settle-seconds 0 \ + --skip-quiet-gate \ + > "${out_root}/solo-driver.log" +jq -e ' + .starsolo_whitelist_sha256 | length == 64 +' "${out_root}/solo-result/contract.json" > /dev/null + +if env -u STARSOLO_WHITELIST \ + "${repo_root}/extras/benchmarks/runGeneralizationMode.sh" \ + starsolo /bin/true "${out_root}/index" \ + "${out_root}/read1.fq" "${out_root}/read2.fq" \ + "${out_root}/missing-whitelist" > "${out_root}/missing.log" 2>&1; then + printf 'STARsolo runner accepted a missing whitelist\n' >&2 + exit 1 +fi +[[ ! -e "${out_root}/missing-whitelist" ]] + +STARSOLO_WHITELIST="${out_root}/whitelist.txt" THREADS=1 \ + "${repo_root}/extras/benchmarks/runGeneralizationMode.sh" \ + starsolo /bin/true "${out_root}/index" \ + "${out_root}/read1.fq" "${out_root}/read2.fq" \ + "${out_root}/solo-command" > "${out_root}/solo-command.log" +grep -Fq -- '--soloUMIlen 12' "${out_root}/solo-command/command.sh" +grep -Eq $'starsolo_whitelist_sha256\t[0-9a-f]{64}' \ + "${out_root}/solo-command/provenance.tsv" + +mkdir -p \ + "${out_root}/solo-a/star.Solo.out/Gene/raw" \ + "${out_root}/solo-b/star.Solo.out/Gene/raw" +for run in solo-a solo-b; do + cat > "${out_root}/${run}/star.Log.final.out" <<'EOF_LOG' +Number of input reads | 1 +Uniquely mapped reads number | 1 +EOF_LOG + printf 'cell\n' > "${out_root}/${run}/star.Solo.out/Gene/raw/barcodes.tsv" + printf 'gene\tgene\tGene Expression\n' \ + > "${out_root}/${run}/star.Solo.out/Gene/raw/features.tsv" + printf '%%%%MatrixMarket matrix coordinate integer general\n1 1 1\n1 1 1\n' \ + > "${out_root}/${run}/star.Solo.out/Gene/raw/matrix.mtx" +done +python3 "${repo_root}/extras/benchmarks/compareGeneralizationRuns.py" \ + starsolo "${out_root}/solo-a" "${out_root}/solo-b" \ + --output "${out_root}/solo-comparison.json" + +printf 'generalization benchmark harness tests passed\n' From 4887f87d94685e7e73222b390c9fb94f1265f726 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 21:27:19 +0000 Subject: [PATCH 08/22] Support plain paired FASTQ benchmark fixtures --- extras/benchmarks/makePairedFastqSubset.py | 33 +++++++++++++++----- extras/tests/scripts/testBenchmarkHarness.sh | 8 +++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/extras/benchmarks/makePairedFastqSubset.py b/extras/benchmarks/makePairedFastqSubset.py index fa1c2551..9a8fc6a9 100755 --- a/extras/benchmarks/makePairedFastqSubset.py +++ b/extras/benchmarks/makePairedFastqSubset.py @@ -1,14 +1,16 @@ #!/usr/bin/env python3 -"""Create a deterministic, validated first-N subset of paired gzip FASTQ files.""" +"""Create a deterministic, validated first-N subset of paired FASTQ files.""" from __future__ import annotations import argparse +from contextlib import contextmanager import gzip import hashlib import os from pathlib import Path import tempfile +from typing import BinaryIO, Iterator def parse_args() -> argparse.Namespace: @@ -21,7 +23,7 @@ def parse_args() -> argparse.Namespace: return parser.parse_args() -def read_record(handle: gzip.GzipFile, label: str, index: int) -> list[bytes] | None: +def read_record(handle: BinaryIO, label: str, index: int) -> list[bytes] | None: lines = [handle.readline() for _ in range(4)] if lines[0] == b"": if any(lines[1:]): @@ -45,13 +47,24 @@ def canonical_name(header: bytes) -> bytes: return name -def open_deterministic_gzip(path: Path) -> tuple[Path, gzip.GzipFile]: +@contextmanager +def open_input(path: Path) -> Iterator[BinaryIO]: + opener = gzip.open if path.suffix == ".gz" else open + with opener(path, "rb") as handle: + yield handle + + +def open_output(path: Path) -> tuple[Path, BinaryIO]: path.parent.mkdir(parents=True, exist_ok=True) fd, raw = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) os.close(fd) temporary = Path(raw) binary = temporary.open("wb") - return temporary, gzip.GzipFile(filename="", mode="wb", fileobj=binary, mtime=0) + if path.suffix == ".gz": + return temporary, gzip.GzipFile( + filename="", mode="wb", fileobj=binary, mtime=0 + ) + return temporary, binary def sha256(path: Path) -> str: @@ -66,14 +79,20 @@ def main() -> int: args = parse_args() if args.records <= 0: raise SystemExit("--records must be positive") + for path in (args.read1, args.read2): + if not path.is_file(): + raise SystemExit(f"input file is absent: {path}") if args.output1.resolve() == args.output2.resolve(): raise SystemExit("mate outputs must be distinct") + input_paths = {args.read1.resolve(), args.read2.resolve()} + if args.output1.resolve() in input_paths or args.output2.resolve() in input_paths: + raise SystemExit("input and output paths must be distinct") - temp1, output1 = open_deterministic_gzip(args.output1) - temp2, output2 = open_deterministic_gzip(args.output2) + temp1, output1 = open_output(args.output1) + temp2, output2 = open_output(args.output2) completed = False try: - with gzip.open(args.read1, "rb") as read1, gzip.open(args.read2, "rb") as read2: + with open_input(args.read1) as read1, open_input(args.read2) as read2: for index in range(1, args.records + 1): record1 = read_record(read1, "read1", index) record2 = read_record(read2, "read2", index) diff --git a/extras/tests/scripts/testBenchmarkHarness.sh b/extras/tests/scripts/testBenchmarkHarness.sh index 075efddd..8ccb057c 100755 --- a/extras/tests/scripts/testBenchmarkHarness.sh +++ b/extras/tests/scripts/testBenchmarkHarness.sh @@ -37,6 +37,14 @@ cmp "${tmp_dir}/subset-r1-1.fastq.gz" "${tmp_dir}/subset-r1-2.fastq.gz" cmp "${tmp_dir}/subset-r2-1.fastq.gz" "${tmp_dir}/subset-r2-2.fastq.gz" [[ "$(gzip -cd "${tmp_dir}/subset-r1-1.fastq.gz" | wc -l)" -eq 8 ]] [[ "$(gzip -cd "${tmp_dir}/subset-r2-1.fastq.gz" | wc -l)" -eq 8 ]] +python3 "${repo_root}/extras/benchmarks/makePairedFastqSubset.py" \ + --read1 "${repo_root}/extras/tests/fixtures/alignment/read1.fastq" \ + --read2 "${repo_root}/extras/tests/fixtures/alignment/read2.fastq" \ + --records 2 \ + --output1 "${tmp_dir}/subset-r1.fastq" \ + --output2 "${tmp_dir}/subset-r2.fastq" +[[ "$(wc -l < "${tmp_dir}/subset-r1.fastq")" -eq 8 ]] +[[ "$(wc -l < "${tmp_dir}/subset-r2.fastq")" -eq 8 ]] for iteration in 1 2; do python3 "${repo_root}/extras/benchmarks/makeFastqSubset.py" \ From cd4ae759bb730594f85458ce38ae611abb2f686b Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 21:37:25 +0000 Subject: [PATCH 09/22] Monitor physical devices in benchmark quiet gate --- extras/benchmarks/quietSystemGate.py | 38 +++++++++++++++++--- extras/tests/scripts/testBenchmarkHarness.sh | 22 ++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/extras/benchmarks/quietSystemGate.py b/extras/benchmarks/quietSystemGate.py index 2edf807e..a3ca5caf 100755 --- a/extras/benchmarks/quietSystemGate.py +++ b/extras/benchmarks/quietSystemGate.py @@ -61,6 +61,28 @@ def device_for_path(path: Path) -> str | None: return None +def physical_devices( + device: str, sys_block: Path = Path("/sys/class/block") +) -> set[str]: + entry = sys_block / device + if not entry.exists(): + return {device} + + slaves = entry / "slaves" + slave_names = sorted(path.name for path in slaves.iterdir()) if slaves.is_dir() else [] + if slave_names: + result: set[str] = set() + for slave in slave_names: + result.update(physical_devices(slave, sys_block)) + return result + + if (entry / "partition").is_file(): + parent = entry.resolve().parent.name + if parent and parent != device: + return physical_devices(parent, sys_block) + return {device} + + def competing_processes(pattern: re.Pattern[str] | None) -> list[tuple[int, str]]: if pattern is None: return [] @@ -88,11 +110,14 @@ def main() -> int: if args.duration <= 0 or args.interval <= 0 or args.interval > args.duration: raise SystemExit("duration and interval must be positive, with interval <= duration") - devices = set(args.device) + logical_devices = set(args.device) for path in args.path: device = device_for_path(path) if device: - devices.add(device) + logical_devices.add(device) + devices: set[str] = set() + for device in logical_devices: + devices.update(physical_devices(device)) process_pattern = re.compile(args.competing_regex) if args.competing_regex else None samples: list[tuple[str, float, float, float, str]] = [] failures: list[str] = [] @@ -135,9 +160,14 @@ def main() -> int: previous_disks = current_disks previous_time = current_time - lines = ["utc\tcpu_idle_percent\tiowait_percent\tstorage_util_percent\tcompetitors"] + device_text = ",".join(sorted(devices)) + lines = [ + "utc\tcpu_idle_percent\tiowait_percent\tstorage_util_percent" + "\tcompetitors\tdevices" + ] lines.extend( - f"{stamp}\t{idle:.3f}\t{iowait:.3f}\t{storage:.3f}\t{competitors}" + f"{stamp}\t{idle:.3f}\t{iowait:.3f}\t{storage:.3f}" + f"\t{competitors}\t{device_text}" for stamp, idle, iowait, storage, competitors in samples ) output = "\n".join(lines) + "\n" diff --git a/extras/tests/scripts/testBenchmarkHarness.sh b/extras/tests/scripts/testBenchmarkHarness.sh index 8ccb057c..51cbb00b 100755 --- a/extras/tests/scripts/testBenchmarkHarness.sh +++ b/extras/tests/scripts/testBenchmarkHarness.sh @@ -65,6 +65,28 @@ python3 "${repo_root}/extras/benchmarks/quietSystemGate.py" \ --min-idle 0 --max-iowait 100 --max-storage-util 1000 \ --path "${tmp_dir}" --output "${tmp_dir}/quiet.tsv" [[ "$(wc -l < "${tmp_dir}/quiet.tsv")" -ge 2 ]] +grep -Fq $'\tdevices' "${tmp_dir}/quiet.tsv" + +python3 - "${repo_root}/extras/benchmarks/quietSystemGate.py" "${tmp_dir}" <<'EOF_DEVICE' +import importlib.util +from pathlib import Path +import sys + +spec = importlib.util.spec_from_file_location("quiet_gate", sys.argv[1]) +module = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = module +spec.loader.exec_module(module) + +root = Path(sys.argv[2]) / "sys-block" +(root / "dm-0" / "slaves").mkdir(parents=True) +(root / "nvme0n1" / "nvme0n1p3").mkdir(parents=True) +(root / "nvme0n1" / "nvme0n1p3" / "partition").write_text("3\n") +(root / "nvme0n1p3").symlink_to(root / "nvme0n1" / "nvme0n1p3") +(root / "dm-0" / "slaves" / "nvme0n1p3").symlink_to( + root / "nvme0n1" / "nvme0n1p3" +) +assert module.physical_devices("dm-0", root) == {"nvme0n1"} +EOF_DEVICE for run in baseline candidate; do mkdir -p "${tmp_dir}/${run}" From a53583962999bdbfc41c6027ca97daebd94bc714 Mon Sep 17 00:00:00 2001 From: Codex Date: Sat, 25 Jul 2026 22:43:52 +0000 Subject: [PATCH 10/22] Run benchmark warmups after quiet gate --- extras/benchmarks/README.md | 5 +++++ extras/benchmarks/runGeneralizationPairs.py | 4 +++- extras/tests/scripts/testGeneralizationHarness.sh | 3 +++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/extras/benchmarks/README.md b/extras/benchmarks/README.md index e4ffd378..b6575507 100644 --- a/extras/benchmarks/README.md +++ b/extras/benchmarks/README.md @@ -134,6 +134,11 @@ practical speedup threshold. The stricter `superiority_2_percent` field is true only when the lower bound of the paired bootstrap interval also clears two percent. Noninferiority must not be described as a speed improvement. +The driver first requires a quiet host, then executes excluded, order-balanced +warmups immediately before measurement. Use representative warmup inputs when +startup state materially affects the workload; use the full corpus when a +small subset does not establish stable timing. + `makeFastqSubset.py` creates a validated, deterministic first-N subset for single-end or long-read FASTQ input. `makePairedFastqSubset.py` additionally checks mate names and should be used for paired or STARsolo fixtures. diff --git a/extras/benchmarks/runGeneralizationPairs.py b/extras/benchmarks/runGeneralizationPairs.py index 58b0e854..25dc7495 100755 --- a/extras/benchmarks/runGeneralizationPairs.py +++ b/extras/benchmarks/runGeneralizationPairs.py @@ -169,6 +169,7 @@ def write_contract( "read2": input_value(args.read2), "read2_sha256": input_digest(args.read2), "warmup_order": warmup_order or "none", + "warmup_position": "after_quiet_gate", "warmup_read1": input_value(args.warmup_read1), "warmup_read1_sha256": input_digest(args.warmup_read1), "warmup_read2": input_value(args.warmup_read2), @@ -274,6 +275,8 @@ def main() -> int: ) write_contract(args, orders, warmup_order) + run_quiet_gate(args) + if warmup_order is not None: roles = ( ("baseline", "candidate") @@ -300,7 +303,6 @@ def main() -> int: ): raise RuntimeError("warmup correctness comparison failed") - run_quiet_gate(args) schedule: list[dict[str, str]] = [] for pair, order in enumerate(orders, 1): roles = ( diff --git a/extras/tests/scripts/testGeneralizationHarness.sh b/extras/tests/scripts/testGeneralizationHarness.sh index c75cc7cb..6dc0d380 100755 --- a/extras/tests/scripts/testGeneralizationHarness.sh +++ b/extras/tests/scripts/testGeneralizationHarness.sh @@ -84,6 +84,9 @@ assert result["median_improvement_percent"] == 10.0 assert result["median_gain_at_least_2_percent"] assert result["superiority_2_percent"] EOF_CHECK +jq -e ' + .warmup_position == "after_quiet_gate" +' "${out_root}/result/contract.json" > /dev/null STARSOLO_WHITELIST="${out_root}/whitelist.txt" \ python3 "${repo_root}/extras/benchmarks/runGeneralizationPairs.py" \ From 4ca5500e7a1affa8decfee5da893ac0a946c4eb3 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 02:18:45 +0000 Subject: [PATCH 11/22] Preserve TranscriptomeSAM primary alignment compatibility --- extras/tests/testReadChunkConfig.cpp | 4 ++++ source/Parameters.cpp | 14 +++++++++++++- source/ReadChunkConfig.cpp | 8 ++++++++ source/ReadChunkConfig.h | 5 +++++ source/parametersDefault | 2 +- 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/extras/tests/testReadChunkConfig.cpp b/extras/tests/testReadChunkConfig.cpp index 7093a07d..9bf5930e 100644 --- a/extras/tests/testReadChunkConfig.cpp +++ b/extras/tests/testReadChunkConfig.cpp @@ -25,6 +25,10 @@ int main() { const std::uint64_t reservePerEnd = 101302; + assert(automaticReadChunkSizingAllowed(1, false)); + assert(!automaticReadChunkSizingAllowed(10, false)); + assert(!automaticReadChunkSizingAllowed(1, true)); + const ReadChunkConfig adaptive = calculateReadChunkConfig( 30000000, 0, 2, 64, reservePerEnd ); diff --git a/source/Parameters.cpp b/source/Parameters.cpp index 5517dbe9..a8219f26 100755 --- a/source/Parameters.cpp +++ b/source/Parameters.cpp @@ -1242,6 +1242,13 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters #else const uint32 chunkInMinimumRecordSlots=1; #endif + // TranscriptomeSAM chooses a primary transcript alignment with a + // per-chunk RNG. Preserve legacy chunking so automatic tuning does not + // change compatibility-visible primary and secondary BAM flags. + const bool adaptiveReadChunksAllowed = automaticReadChunkSizingAllowed( + readFilesTypeN, + quant.trSAM.bamYes + ); ReadChunkConfig readChunkConfig; try { readChunkConfig = calculateReadChunkConfig( @@ -1250,7 +1257,7 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters readNends, runThreadN, chunkInReservePerEnd, - readFilesTypeN!=10, + adaptiveReadChunksAllowed, chunkInMinimumRecordSlots ); } catch (const std::invalid_argument &error) { @@ -1272,6 +1279,11 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters << chunkInSizeBytesArray << " bytes per end, mode=" << (readChunkConfig.adaptive ? "adaptive" : "configured") << '\n'; + if (quant.trSAM.bamYes && readChunkSizeBytes==0 && runThreadN>=64) { + inOut->logMain + << "Automatic read chunk sizing disabled for TranscriptomeSAM " + << "primary-alignment compatibility\n"; + } ///////////////////////////////////////////////////////// outSJ diff --git a/source/ReadChunkConfig.cpp b/source/ReadChunkConfig.cpp index ab1018c3..657aeee0 100644 --- a/source/ReadChunkConfig.cpp +++ b/source/ReadChunkConfig.cpp @@ -18,6 +18,14 @@ std::invalid_argument invalidValue(const std::string &message) } } +bool automaticReadChunkSizingAllowed( + std::uint32_t readFilesType, + bool transcriptomeBam +) +{ + return readFilesType != 10 && !transcriptomeBam; +} + ReadChunkConfig calculateReadChunkConfig( std::uint64_t maximumTotalBytes, std::uint64_t requestedTotalBytes, diff --git a/source/ReadChunkConfig.h b/source/ReadChunkConfig.h index 82f7a188..60c932b9 100644 --- a/source/ReadChunkConfig.h +++ b/source/ReadChunkConfig.h @@ -11,6 +11,11 @@ struct ReadChunkConfig { bool adaptive; }; +bool automaticReadChunkSizingAllowed( + std::uint32_t readFilesType, + bool transcriptomeBam +); + ReadChunkConfig calculateReadChunkConfig( std::uint64_t maximumTotalBytes, std::uint64_t requestedTotalBytes, diff --git a/source/parametersDefault b/source/parametersDefault index 72f49427..4351b237 100755 --- a/source/parametersDefault +++ b/source/parametersDefault @@ -250,7 +250,7 @@ limitIObufferSize 30000000 50000000 int(s)>0: max available buffers size (bytes) for input/output, per thread readChunkSizeBytes 0 - int>=0: target input chunk buffer size (bytes), total across mates, per thread. 0 uses 1000000 bytes for >=64 mapping threads and limitIObufferSize otherwise + int>=0: target input chunk buffer size (bytes), total across mates, per thread. 0 uses 1000000 bytes for >=64 mapping threads and limitIObufferSize otherwise; automatic sizing is disabled for SAM input and TranscriptomeSAM BAM compatibility limitOutSAMoneReadBytes 100000 int>0: max size of the SAM record (bytes) for one read. Recommended value: >(2*(LengthMate1+LengthMate2+100)*outFilterMultimapNmax From ef2a2560013293a3cd93403d876f50a3d5ec759c Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 02:47:59 +0000 Subject: [PATCH 12/22] Make TranscriptomeSAM primary selection deterministic --- .github/workflows/blackstar-ci.yml | 1 + extras/benchmarks/compareAlignmentRuns.py | 10 ++- .../benchmarks/compareGeneralizationRuns.py | 20 +++++- extras/benchmarks/runGeneralizationPairs.py | 51 ++++++++++++++- .../scripts/testGeneralizationHarness.sh | 46 ++++++++++++++ extras/tests/scripts/testSpecializedModes.sh | 62 +++++++++++++++++-- .../tests/scripts/testTranscriptomePrimary.sh | 15 +++++ extras/tests/testReadChunkConfig.cpp | 5 +- extras/tests/testTranscriptomePrimary.cpp | 32 ++++++++++ source/Makefile | 2 +- source/Parameters.cpp | 14 +---- source/ReadAlign_quantTranscriptome.cpp | 15 ++++- source/ReadChunkConfig.cpp | 5 +- source/ReadChunkConfig.h | 3 +- source/TranscriptomePrimary.cpp | 28 +++++++++ source/TranscriptomePrimary.h | 12 ++++ source/parametersDefault | 4 +- 17 files changed, 289 insertions(+), 36 deletions(-) create mode 100755 extras/tests/scripts/testTranscriptomePrimary.sh create mode 100644 extras/tests/testTranscriptomePrimary.cpp create mode 100644 source/TranscriptomePrimary.cpp create mode 100644 source/TranscriptomePrimary.h diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index 95118842..0de8bdc0 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -138,6 +138,7 @@ jobs: extras/tests/scripts/testAlignmentThreadAffinity.sh extras/tests/scripts/testNumaMemoryPolicy.sh extras/tests/scripts/testReadChunkConfig.sh + extras/tests/scripts/testTranscriptomePrimary.sh extras/tests/scripts/testSystemMemory.sh extras/tests/scripts/testPackedArray.sh extras/tests/scripts/testSuffixComparator.sh diff --git a/extras/benchmarks/compareAlignmentRuns.py b/extras/benchmarks/compareAlignmentRuns.py index 6bee2c7e..de829b5e 100755 --- a/extras/benchmarks/compareAlignmentRuns.py +++ b/extras/benchmarks/compareAlignmentRuns.py @@ -81,7 +81,9 @@ def canonical_bam_header(path: Path, samtools: str) -> bytes: return ("\n".join(headers) + "\n").encode() -def canonical_bam_digest(path: Path, temp_root: Path) -> str: +def canonical_bam_digest( + path: Path, temp_root: Path, remove_flags: int = 0 +) -> str: samtools = shutil.which("samtools") sort_bin = shutil.which("sort") if not samtools or not sort_bin: @@ -90,7 +92,11 @@ def canonical_bam_digest(path: Path, temp_root: Path) -> str: value.update(canonical_bam_header(path, samtools)) value.update(b"\0alignment-records\0") env = dict(os.environ, LC_ALL="C") - view = subprocess.Popen([samtools, "view", str(path)], stdout=subprocess.PIPE) + view_command = [samtools, "view"] + if remove_flags: + view_command.extend(("--remove-flags", str(remove_flags))) + view_command.append(str(path)) + view = subprocess.Popen(view_command, stdout=subprocess.PIPE) sorter = subprocess.Popen( [sort_bin, "-T", str(temp_root), "-S", "2G"], stdin=view.stdout, diff --git a/extras/benchmarks/compareGeneralizationRuns.py b/extras/benchmarks/compareGeneralizationRuns.py index b1d06783..a7fb8e58 100755 --- a/extras/benchmarks/compareGeneralizationRuns.py +++ b/extras/benchmarks/compareGeneralizationRuns.py @@ -212,12 +212,22 @@ def main() -> int: ) as raw: temp_root = Path(raw) for name in bam_names: + remove_flags = ( + 0x100 + if name == "star.Aligned.toTranscriptome.out.bam" + else 0 + ) + label = f"canonical {name} records" + if remove_flags: + label += " ignoring primary/secondary choice" add_presence_digest( checks, - f"canonical {name} records", + label, baseline / name, candidate / name, - lambda path, root=temp_root: canonical_bam_digest(path, root), + lambda path, root=temp_root, flags=remove_flags: ( + canonical_bam_digest(path, root, flags) + ), ) if args.mode == "starsolo": @@ -228,6 +238,12 @@ def main() -> int: "mode": args.mode, "baseline": str(baseline), "candidate": str(candidate), + "transcriptome_primary_oracle": ( + "alignment records must match after clearing SAM flag 0x100; " + "candidate repeatability is qualified separately" + if args.mode == "transcriptome-bam" + else "not-applicable" + ), "passed": bool(checks) and all(bool(check["passed"]) for check in checks), "checks": checks, } diff --git a/extras/benchmarks/runGeneralizationPairs.py b/extras/benchmarks/runGeneralizationPairs.py index 25dc7495..4e452873 100755 --- a/extras/benchmarks/runGeneralizationPairs.py +++ b/extras/benchmarks/runGeneralizationPairs.py @@ -12,8 +12,10 @@ import statistics import subprocess import sys +import tempfile import time +from compareAlignmentRuns import canonical_bam_digest from runAlignmentPairs import ( bootstrap_median_ci, coefficient_of_variation, @@ -176,6 +178,12 @@ def write_contract( "warmup_read2_sha256": input_digest(args.warmup_read2), "starsolo_whitelist": input_value(whitelist), "starsolo_whitelist_sha256": input_digest(whitelist), + "transcriptome_primary_oracle": ( + "upstream-equivalent records after clearing SAM flag 0x100; " + "exact candidate primary flags across all candidate runs" + if args.mode == "transcriptome-bam" + else "not-applicable" + ), "tools": { "pair_driver_sha256": sha256(Path(__file__)), "runner_sha256": sha256(args.runner), @@ -277,13 +285,13 @@ def main() -> int: run_quiet_gate(args) + warmups: dict[str, Path] = {} if warmup_order is not None: roles = ( ("baseline", "candidate") if warmup_order == "AB" else ("candidate", "baseline") ) - warmups: dict[str, Path] = {} assert args.warmup_read1 is not None for position, role in enumerate(roles, 1): output = args.output / f"warmup-{position}-{role}" @@ -388,6 +396,42 @@ def main() -> int: } ) + candidate_primary_digests: list[dict[str, str]] = [] + candidate_primary_deterministic = True + if args.mode == "transcriptome-bam": + candidate_runs = [ + args.output / row["run_dir"] + for row in schedule + if row["role"] == "candidate" + ] + if "candidate" in warmups: + candidate_runs.insert(0, warmups["candidate"]) + parent = args.output + with tempfile.TemporaryDirectory( + prefix="blackstar-transcriptome-primary-", dir=parent + ) as raw: + temp_root = Path(raw) + for run in candidate_runs: + primary_digest = canonical_bam_digest( + run / "star.Aligned.toTranscriptome.out.bam", + temp_root, + ) + candidate_primary_digests.append( + { + "run": run.name, + "sha256": primary_digest, + } + ) + candidate_primary_deterministic = ( + bool(candidate_primary_digests) + and len( + { + item["sha256"] + for item in candidate_primary_digests + } + ) == 1 + ) + ci_low, ci_high = bootstrap_median_ci(improvements, args.seed ^ 0x6E) baseline_cv = coefficient_of_variation(baseline_times) candidate_cv = coefficient_of_variation(candidate_times) @@ -405,6 +449,10 @@ def main() -> int: "wall_time_noninferiority": ci_low >= -args.margin_percent, "rss": rss_increase <= args.max_rss_increase_percent, } + if args.mode == "transcriptome-bam": + gates["candidate_transcriptome_primary_determinism"] = ( + candidate_primary_deterministic + ) result = { "schema": "blackstar-generalization-result-v1", "mode": args.mode, @@ -421,6 +469,7 @@ def main() -> int: "median_rss_increase_percent": rss_increase, "median_gain_at_least_2_percent": statistics.median(improvements) >= 2.0, "superiority_2_percent": ci_low >= 2.0, + "candidate_transcriptome_primary_digests": candidate_primary_digests, } (args.output / "result.json").write_text( json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8" diff --git a/extras/tests/scripts/testGeneralizationHarness.sh b/extras/tests/scripts/testGeneralizationHarness.sh index 6dc0d380..135a3820 100755 --- a/extras/tests/scripts/testGeneralizationHarness.sh +++ b/extras/tests/scripts/testGeneralizationHarness.sh @@ -54,6 +54,25 @@ if [[ "${mode}" == "starsolo" ]]; then printf '%%%%MatrixMarket matrix coordinate integer general\n1 1 1\n1 1 1\n' \ > "${out}/star.Solo.out/Gene/raw/matrix.mtx" fi +if [[ "${mode}" == "transcriptome-bam" ]]; then + cat > "${out}/main.sam" <<'EOF_MAIN' +@HD VN:1.6 SO:unsorted +read1 4 * 0 0 * * 0 0 ACGT IIII +EOF_MAIN + if [[ "$(basename "${binary}")" == "baseline" ]]; then + transcriptome_flag=260 + else + transcriptome_flag=4 + fi + cat > "${out}/transcriptome.sam" < "${out}/star.Aligned.out.bam" + samtools view -bS "${out}/transcriptome.sam" \ + > "${out}/star.Aligned.toTranscriptome.out.bam" +fi EOF_RUNNER chmod +x "${out_root}/fake-runner" @@ -107,6 +126,33 @@ jq -e ' .starsolo_whitelist_sha256 | length == 64 ' "${out_root}/solo-result/contract.json" > /dev/null +python3 "${repo_root}/extras/benchmarks/runGeneralizationPairs.py" \ + --mode transcriptome-bam \ + --baseline-bin "${out_root}/baseline" \ + --candidate-bin "${out_root}/candidate" \ + --genome-dir "${out_root}/index" \ + --read1 "${out_root}/read1.fq" \ + --read2 "${out_root}/read2.fq" \ + --output "${out_root}/transcriptome-result" \ + --runner "${out_root}/fake-runner" \ + --threads 4 \ + --pairs 3 \ + --settle-seconds 0 \ + --skip-quiet-gate \ + > "${out_root}/transcriptome-driver.log" +jq -e ' + .accepted and + .gates.correctness and + .gates.candidate_transcriptome_primary_determinism and + ([.candidate_transcriptome_primary_digests[].sha256] | unique | length) == 1 +' "${out_root}/transcriptome-result/result.json" > /dev/null +jq -e ' + .passed and + ([.checks[] | + select(.name | contains("ignoring primary/secondary choice"))] | + length) == 1 +' "${out_root}/transcriptome-result/pair-01-comparison.json" > /dev/null + if env -u STARSOLO_WHITELIST \ "${repo_root}/extras/benchmarks/runGeneralizationMode.sh" \ starsolo /bin/true "${out_root}/index" \ diff --git a/extras/tests/scripts/testSpecializedModes.sh b/extras/tests/scripts/testSpecializedModes.sh index 86a5d4bf..713cc8ba 100755 --- a/extras/tests/scripts/testSpecializedModes.sh +++ b/extras/tests/scripts/testSpecializedModes.sh @@ -116,6 +116,8 @@ with (root / "genome.fa").open("w", encoding="ascii", newline="\n") as handle: ( 'chrA\tfixture\texon\t10001\t10300\t.\t+\t.\tgene_id "geneA"; transcript_id "txA";', 'chrA\tfixture\texon\t11001\t11300\t.\t+\t.\tgene_id "geneA"; transcript_id "txA";', + 'chrA\tfixture\texon\t10001\t10300\t.\t+\t.\tgene_id "geneA"; transcript_id "txA_alt";', + 'chrA\tfixture\texon\t11001\t11300\t.\t+\t.\tgene_id "geneA"; transcript_id "txA_alt";', 'chrA\tfixture\texon\t90001\t90600\t.\t+\t.\tgene_id "geneW"; transcript_id "txW";', 'chrB\tfixture\texon\t30001\t30600\t.\t+\t.\tgene_id "geneB"; transcript_id "txB";', ) @@ -230,6 +232,12 @@ normalize_bam() { samtools view "${input}" | LC_ALL=C sort > "${output}" } +normalize_bam_without_secondary() { + local input="$1" + local output="$2" + samtools view --remove-flags 0x100 "${input}" | LC_ALL=C sort > "${output}" +} + normalize_text() { local input="$1" local output="$2" @@ -241,7 +249,7 @@ compare_exact() { local first="$2" local second="$3" if ! cmp -s "${first}" "${second}"; then - echo "ERROR: ${label} differs between official STAR and BlackSTAR" >&2 + echo "ERROR: ${label} differs" >&2 diff -u "${first}" "${second}" | sed -n '1,160p' >&2 || true exit 1 fi @@ -285,17 +293,58 @@ prepare_mode paired --quantMode TranscriptomeSAM GeneCounts \ --outFileNamePrefix "${out_root}/paired/blackstar/" \ > "${out_root}/paired/blackstar.stdout" 2>&1 +mkdir -p "${out_root}/paired/blackstar-repeat" +"${star_bin}" \ + --runThreadN 1 \ + --genomeDir "${base_index}" \ + --readFilesIn "${out_root}/inputs/paired_R1.fq" "${out_root}/inputs/paired_R2.fq" \ + --outSAMtype SAM \ + --outSAMattributes Standard \ + --quantMode TranscriptomeSAM GeneCounts \ + --outFileNamePrefix "${out_root}/paired/blackstar-repeat/" \ + > "${out_root}/paired/blackstar-repeat.stdout" 2>&1 compare_sam_outputs paired -normalize_bam \ +normalize_bam_without_secondary \ "${out_root}/paired/upstream/Aligned.toTranscriptome.out.bam" \ - "${out_root}/paired/upstream/transcriptome.sorted.sam" + "${out_root}/paired/upstream/transcriptome.without-secondary.sorted.sam" +normalize_bam_without_secondary \ + "${out_root}/paired/blackstar/Aligned.toTranscriptome.out.bam" \ + "${out_root}/paired/blackstar/transcriptome.without-secondary.sorted.sam" +compare_exact \ + "paired transcriptome BAM alignment set" \ + "${out_root}/paired/upstream/transcriptome.without-secondary.sorted.sam" \ + "${out_root}/paired/blackstar/transcriptome.without-secondary.sorted.sam" normalize_bam \ "${out_root}/paired/blackstar/Aligned.toTranscriptome.out.bam" \ "${out_root}/paired/blackstar/transcriptome.sorted.sam" +normalize_bam \ + "${out_root}/paired/blackstar-repeat/Aligned.toTranscriptome.out.bam" \ + "${out_root}/paired/blackstar-repeat/transcriptome.sorted.sam" compare_exact \ - "paired transcriptome BAM records" \ - "${out_root}/paired/upstream/transcriptome.sorted.sam" \ - "${out_root}/paired/blackstar/transcriptome.sorted.sam" + "paired transcriptome BAM deterministic primary flags" \ + "${out_root}/paired/blackstar/transcriptome.sorted.sam" \ + "${out_root}/paired/blackstar-repeat/transcriptome.sorted.sam" +if ! samtools view \ + "${out_root}/paired/blackstar/Aligned.toTranscriptome.out.bam" | + awk ' + { + total[$1]++ + if (int($2 / 256) % 2 == 0) { + primary[$1]++ + } + } + END { + for (name in total) { + if (total[name] > 2 && primary[name] == 2) { + found=1 + } + } + exit found ? 0 : 1 + } + '; then + echo "ERROR: transcriptome fixture did not exercise ambiguous primary selection" >&2 + exit 1 +fi compare_exact \ "paired gene counts" \ "${out_root}/paired/upstream/ReadsPerGene.out.tab" \ @@ -624,6 +673,7 @@ cat <<'EOF_RESULTS' check status paired_fragmented_alignment pass transcriptome_bam pass +transcriptome_primary_determinism pass gene_counts pass two_pass_mapping pass bysjout_filtering pass diff --git a/extras/tests/scripts/testTranscriptomePrimary.sh b/extras/tests/scripts/testTranscriptomePrimary.sh new file mode 100755 index 00000000..4b7f22f6 --- /dev/null +++ b/extras/tests/scripts/testTranscriptomePrimary.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +build_dir="$(mktemp -d "${TMPDIR:-/tmp}/blackstar-transcriptome-primary-test.XXXXXX")" +trap 'rm -rf "${build_dir}"' EXIT + +"${CXX:-g++}" \ + -std=c++11 -Wall -Wextra -fsanitize=address,undefined \ + -I"${repo_root}/source" \ + "${repo_root}/extras/tests/testTranscriptomePrimary.cpp" \ + "${repo_root}/source/TranscriptomePrimary.cpp" \ + -o "${build_dir}/testTranscriptomePrimary" + +ASAN_OPTIONS=detect_leaks=0 "${build_dir}/testTranscriptomePrimary" diff --git a/extras/tests/testReadChunkConfig.cpp b/extras/tests/testReadChunkConfig.cpp index 9bf5930e..283d6511 100644 --- a/extras/tests/testReadChunkConfig.cpp +++ b/extras/tests/testReadChunkConfig.cpp @@ -25,9 +25,8 @@ int main() { const std::uint64_t reservePerEnd = 101302; - assert(automaticReadChunkSizingAllowed(1, false)); - assert(!automaticReadChunkSizingAllowed(10, false)); - assert(!automaticReadChunkSizingAllowed(1, true)); + assert(automaticReadChunkSizingAllowed(1)); + assert(!automaticReadChunkSizingAllowed(10)); const ReadChunkConfig adaptive = calculateReadChunkConfig( 30000000, 0, 2, 64, reservePerEnd diff --git a/extras/tests/testTranscriptomePrimary.cpp b/extras/tests/testTranscriptomePrimary.cpp new file mode 100644 index 00000000..9750467e --- /dev/null +++ b/extras/tests/testTranscriptomePrimary.cpp @@ -0,0 +1,32 @@ +#include "TranscriptomePrimary.h" + +#include +#include +#include +#include + +int main() +{ + assert(transcriptomePrimaryIndex(777, 1, 0) == 0); + assert(transcriptomePrimaryIndex(777, 1, 1) == 0); + + const std::uint32_t first = + transcriptomePrimaryIndex(777, 123456789ULL, 17); + assert(first == 9); + assert(first == transcriptomePrimaryIndex(777, 123456789ULL, 17)); + assert(transcriptomePrimaryIndex(778, 123456789ULL, 17) == 3); + assert(transcriptomePrimaryIndex(777, 123456790ULL, 17) == 6); + assert(transcriptomePrimaryIndex(0, UINT64_MAX, 31) == 17); + + std::array buckets = {{0, 0, 0, 0, 0, 0, 0}}; + for (std::uint64_t readIndex = 1; readIndex <= 70000; ++readIndex) { + ++buckets[transcriptomePrimaryIndex(777, readIndex, buckets.size())]; + } + for (std::uint32_t count : buckets) { + assert(count > 9500); + assert(count < 10500); + } + + std::cout << "transcriptome primary-selection tests passed\n"; + return 0; +} diff --git a/source/Makefile b/source/Makefile index 93812df9..88dd6d3c 100644 --- a/source/Makefile +++ b/source/Makefile @@ -91,7 +91,7 @@ OBJECTS = systemFunctions.o funPrimaryAlignMark.o \ Genome_genomeGenerate.o genomeParametersWrite.o genomeScanFastaFiles.o genomeSAindex.o \ Genome_insertSequences.o Genome_validateGenomeInsertAnnotations.o Genome_writeGenomeIndex.o insertSeqSA.o funCompareUintAndSuffixes.o funCompareUintAndSuffixesMemcmp.o \ TimeFunctions.o ErrorWarning.o streamFuns.o stringSubstituteAll.o \ - Transcriptome.o Transcriptome_quantAlign.o Transcriptome_geneFullAlignOverlap.o \ + Transcriptome.o Transcriptome_quantAlign.o Transcriptome_geneFullAlignOverlap.o TranscriptomePrimary.o \ ReadAlign_quantTranscriptome.o Quantifications.o Transcriptome_geneCountsAddAlign.o \ sjdbLoadFromFiles.o sjdbLoadFromStream.o sjdbPrepare.o sjdbBuildIndex.o sjdbInsertJunctions.o AlignmentThreadAffinity.o NumaMemoryPolicy.o ReadChunkConfig.o SystemMemory.o mapThreadsSpawn.o \ Parameters_readFilesInit.o Parameters_openReadsFiles.cpp Parameters_closeReadsFiles.cpp Parameters_readSAMheader.o \ diff --git a/source/Parameters.cpp b/source/Parameters.cpp index a8219f26..79f6046c 100755 --- a/source/Parameters.cpp +++ b/source/Parameters.cpp @@ -1242,13 +1242,8 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters #else const uint32 chunkInMinimumRecordSlots=1; #endif - // TranscriptomeSAM chooses a primary transcript alignment with a - // per-chunk RNG. Preserve legacy chunking so automatic tuning does not - // change compatibility-visible primary and secondary BAM flags. - const bool adaptiveReadChunksAllowed = automaticReadChunkSizingAllowed( - readFilesTypeN, - quant.trSAM.bamYes - ); + const bool adaptiveReadChunksAllowed = + automaticReadChunkSizingAllowed(readFilesTypeN); ReadChunkConfig readChunkConfig; try { readChunkConfig = calculateReadChunkConfig( @@ -1279,11 +1274,6 @@ void Parameters::inputParameters (int argInN, char* argIn[]) {//input parameters << chunkInSizeBytesArray << " bytes per end, mode=" << (readChunkConfig.adaptive ? "adaptive" : "configured") << '\n'; - if (quant.trSAM.bamYes && readChunkSizeBytes==0 && runThreadN>=64) { - inOut->logMain - << "Automatic read chunk sizing disabled for TranscriptomeSAM " - << "primary-alignment compatibility\n"; - } ///////////////////////////////////////////////////////// outSJ diff --git a/source/ReadAlign_quantTranscriptome.cpp b/source/ReadAlign_quantTranscriptome.cpp index 6574a6ab..ef2ab0a4 100644 --- a/source/ReadAlign_quantTranscriptome.cpp +++ b/source/ReadAlign_quantTranscriptome.cpp @@ -1,8 +1,8 @@ #include "Transcriptome.h" #include "ReadAlign.h" #include "Transcript.h" +#include "TranscriptomePrimary.h" #include "serviceFuns.cpp" -#include uint ReadAlign::quantTranscriptome (Transcriptome *Tr, uint nAlignG, Transcript **alignG, Transcript *alignT) { uint nAlignT=0; @@ -66,7 +66,18 @@ uint ReadAlign::quantTranscriptome (Transcriptome *Tr, uint nAlignG, Transcript }; if (P.quant.trSAM.bamYes) {//output Aligned.toTranscriptome.bam - alignT[int(rngUniformReal0to1(rngMultOrder)*nAlignT)].primaryFlag=true; + // Keep the inherited RNG stream position unchanged for later reads; + // only the compatibility-visible transcript primary choice is made + // independently of worker scheduling. + (void) rngUniformReal0to1(rngMultOrder); + if (nAlignT > 0) { + // STAR's thread-local RNG makes this flag depend on which worker + // acquires a read. Select from the same alignment set using only + // stable run and read identities instead. + alignT[transcriptomePrimaryIndex( + P.runRNGseed, iReadAll, nAlignT + )].primaryFlag=true; + } for (uint iatr=0;iatr> 30)) * 0xbf58476d1ce4e5b9ULL; + value = (value ^ (value >> 27)) * 0x94d049bb133111ebULL; + return value ^ (value >> 31); +} +} + +std::uint32_t transcriptomePrimaryIndex( + int runSeed, + std::uint64_t readIndex, + std::uint32_t alignmentCount +) +{ + if (alignmentCount == 0) { + return 0; + } + + const std::uint64_t seed = + static_cast(static_cast(runSeed)); + const std::uint64_t randomValue = + splitMix64(readIndex ^ (seed << 32) ^ seed); + return static_cast(randomValue % alignmentCount); +} diff --git a/source/TranscriptomePrimary.h b/source/TranscriptomePrimary.h new file mode 100644 index 00000000..b50051b1 --- /dev/null +++ b/source/TranscriptomePrimary.h @@ -0,0 +1,12 @@ +#ifndef H_TranscriptomePrimary +#define H_TranscriptomePrimary + +#include + +std::uint32_t transcriptomePrimaryIndex( + int runSeed, + std::uint64_t readIndex, + std::uint32_t alignmentCount +); + +#endif diff --git a/source/parametersDefault b/source/parametersDefault index 4351b237..302a4439 100755 --- a/source/parametersDefault +++ b/source/parametersDefault @@ -30,7 +30,7 @@ runDirPerm User_RWX All_RWX ... all-read/write/execute (same as chmod 777) runRNGseed 777 - int: random number generator seed. + int: random number generator seed; also seeds deterministic primary-alignment selection for TranscriptomeSAM output. ### Genome Parameters @@ -250,7 +250,7 @@ limitIObufferSize 30000000 50000000 int(s)>0: max available buffers size (bytes) for input/output, per thread readChunkSizeBytes 0 - int>=0: target input chunk buffer size (bytes), total across mates, per thread. 0 uses 1000000 bytes for >=64 mapping threads and limitIObufferSize otherwise; automatic sizing is disabled for SAM input and TranscriptomeSAM BAM compatibility + int>=0: target input chunk buffer size (bytes), total across mates, per thread. 0 uses 1000000 bytes for >=64 mapping threads and limitIObufferSize otherwise; automatic sizing is disabled for SAM input limitOutSAMoneReadBytes 100000 int>0: max size of the SAM record (bytes) for one read. Recommended value: >(2*(LengthMate1+LengthMate2+100)*outFilterMultimapNmax From 8696f747e7490f3e8870392468b680550e56476a Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 04:36:24 +0000 Subject: [PATCH 13/22] Document cross-workload BlackSTAR qualification --- CHANGELOG.md | 19 ++ docs/COMPATIBILITY.md | 17 ++ docs/PERFORMANCE.md | 41 ++++ docs/architecture/README.md | 2 + docs/architecture/STATUS_HISTORY.md | 2 + docs/architecture/claims.tsv | 20 ++ .../pdf/F27-cross-workload-generalization.pdf | Bin 0 -> 30636 bytes .../F28-transcriptome-primary-determinism.pdf | Bin 0 -> 24281 bytes .../src/F27-cross-workload-generalization.mmd | 46 ++++ .../F28-transcriptome-primary-determinism.mmd | 34 +++ .../svg/F27-cross-workload-generalization.svg | 2 + .../F28-transcriptome-primary-determinism.svg | 2 + .../alignment-Q02-cross-workload-20260726.tsv | 34 +++ docs/architecture/figures.json | 24 ++ .../Q02-cross-workload-generalization.md | 227 ++++++++++++++++++ docs/experiments/README.md | 3 + docs/experiments/ROADMAP.md | 7 + 17 files changed, 480 insertions(+) create mode 100644 docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf create mode 100644 docs/architecture/diagrams/pdf/F28-transcriptome-primary-determinism.pdf create mode 100644 docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd create mode 100644 docs/architecture/diagrams/src/F28-transcriptome-primary-determinism.mmd create mode 100644 docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg create mode 100644 docs/architecture/diagrams/svg/F28-transcriptome-primary-determinism.svg create mode 100644 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv create mode 100644 docs/experiments/Q02-cross-workload-generalization.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 406784ab..db131036 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ This changelog records BlackSTAR project releases. The inherited STAR history remains available in `CHANGES.md` and `RELEASEnotes.md`. +## Unreleased + +- Harden SAM-input chunk sizing and genome-insert annotation and identity + validation. +- Bound automatic index strategies by cgroup-aware available memory. +- Restore inherited NUMA policy after private genome loading. +- Isolate short-read and STARlong build state and add explicit baseline x86-64 + and AVX2 release variants. +- Add differential and paired benchmark coverage for fragmented, single-end, + two-pass, BySJout, chimeric, sorted-BAM, transcriptome-BAM, STARsolo, and + STARlong modes. +- Make TranscriptomeSAM primary-alignment flags deterministic across worker + schedules while preserving the complete alignment set and later inherited + random-stream position. + +These changes remain outside the current release boundary. The Q02 single-end +timing series failed its variability gate, and neither a release nor an external +deployment is authorized by this entry. + ## 1.0.0 - 2026-07-24 - Transition project identity from a GitHub fork to an independently maintained diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index e9c9c183..8f5f583a 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -81,6 +81,23 @@ Changes caused by explicit BlackSTAR-only references are expected. In a base-versus-Delta test, noninserted biological output must remain equivalent outside mappings and counts attributable to requested added references. +### Unreleased TranscriptomeSAM Correction + +The Q02 Labs candidate makes the `TranscriptomeSAM` primary-transcript flag +deterministic from `runRNGseed` and the stable input-read ordinal. Official STAR +uses a worker-local random generator for this choice, so a controlled run on +the same reads produced different raw primary flags at 1 and 96 threads. + +BlackSTAR preserves the complete transcript alignment set, genomic BAM records, +gene counts, splice junctions, and timing-independent metrics. It may assign +flag `0x100` to a different member of an otherwise identical transcript +alignment set than one particular official STAR run. One inherited random draw +is retained per read so later inherited random choices do not shift. + +This correction is present only in the unreleased Labs candidate documented by +[Q02](experiments/Q02-cross-workload-generalization.md). It is not a promise of +the current stable release until deliberately promoted and versioned. + ## Resource and Runtime Behavior Wall time, peak memory, CPU placement, NUMA allocation, I/O scheduling, and diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 2467067d..1148440d 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -75,6 +75,47 @@ All 57 cold-versus-warm artifact checks passed. Full and Delta alignment matched timing-independent metrics, junctions, gene counts, and canonical BAM records, with exactly 100 GFP and 100 GST fragments counted in each mode. +## Unreleased Cross-Workload Labs Evidence + +Q02 compared official STAR 2.7.11b with an unreleased BlackSTAR hardening +candidate across common modes beyond paired gene-count-only alignment. These +results are not part of the current release boundary and are not incremental +effects of Q02 alone; most reflect the cumulative BlackSTAR runtime stack. + +| Workload | Official STAR | BlackSTAR | Median paired change | 95% interval | Qualification | +| --- | ---: | ---: | ---: | ---: | --- | +| Paired 76-base fragmented | 72.02 s | 58.02 s | +20.05% | +16.32 to +23.96% | Compatibility pass | +| Paired 150-base fragmented | 89.77 s | 78.66 s | +12.67% | +11.38 to +13.46% | All gates pass | +| Single-end 150-base | 61.52 s | 58.95 s | +1.67% | -1.87 to +13.42% | **CV gate failed; no speed claim** | +| Two-pass Basic | 223.91 s | 177.05 s | +20.25% | +15.44 to +20.93% | Compatibility pass | +| BySJout | 91.36 s | 62.16 s | +31.45% | +31.22 to +35.64% | All gates pass | +| Chimeric detection | 89.55 s | 60.09 s | +31.76% | +30.98 to +34.00% | All gates pass | +| Coordinate-sorted BAM | 106.56 s | 85.31 s | +18.42% | +16.93 to +24.26% | Compatibility pass | +| Genomic plus transcriptome BAM | 217.33 s | 213.58 s | +1.55% | -0.45 to +2.63% | Noninferior; no speed claim | +| STARsolo 10x v3 Gene | 153.45 s | 149.23 s | +3.78% | -1.43 to +10.49% | Noninferior; no speed claim | +| STARlong direct RNA | 72.79 s | 58.61 s | +19.48% | +17.11 to +22.38% | All gates pass with symmetric seed override | + +Positive change means less BlackSTAR wall time. Every one of the 36/36 +mode-specific pair comparisons passed its applicable output oracle, including +the pairs in the nonaccepted single-end timing series. Candidate peak RSS was +lower in every row. + +The paired 76-base, two-pass, and sorted-BAM control arms had CV between 3 and +5 percent. Their positive intervals are useful generalization evidence, but +they do not satisfy the separate Labs preference for less than 3 percent arm +CV for a new speed claim. STARsolo and TranscriptomeSAM establish only +noninferiority. + +The STARlong comparison used `--seedPerReadNmax 100000` in both arms. Both +official STARlong and BlackSTAR STARlong abort on this real direct-RNA fixture +under the inherited default. Single-end performance also remains unresolved: +the dedicated-host five-pair result passed correctness, RSS, and 2 percent +noninferiority, but candidate CV was 5.22 percent. + +See [Q02](experiments/Q02-cross-workload-generalization.md) for exact binary +identities, fixtures, resource results, negative evidence, and the +TranscriptomeSAM determinism correction. + ## Evidence Bounded machine-readable receipts are committed under diff --git a/docs/architecture/README.md b/docs/architecture/README.md index ec5c2fc6..567f5fa0 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -33,6 +33,8 @@ customer-derived evidence belong in an untracked internal derivative. 24. [Independent-successor transition](diagrams/svg/F24-independent-successor.svg) 25. [Version and compatibility identities](diagrams/svg/F25-version-compatibility.svg) 26. [Governance and release path](diagrams/svg/F26-governance-release.svg) +27. [Q02 cross-workload generalization](diagrams/svg/F27-cross-workload-generalization.svg) +28. [Transcriptome primary determinism](diagrams/svg/F28-transcriptome-primary-determinism.svg) The editable sources are under `diagrams/src/`. The generated SVG and PDF exports are presentation-ready but are never the source of truth. diff --git a/docs/architecture/STATUS_HISTORY.md b/docs/architecture/STATUS_HISTORY.md index 7ceb8bb0..555a7826 100644 --- a/docs/architecture/STATUS_HISTORY.md +++ b/docs/architecture/STATUS_HISTORY.md @@ -24,6 +24,8 @@ not be described as shipped behavior. | A06 transcript recursion copy elision | Accepted | blackstar.2 | Exact commit 6032393 improved the five-pair uncompressed full-corpus median by 2.29% over A05 with a positive paired interval, exact outputs, compressed-input support, a canonical BAM pass, and Q01 cumulative qualification. | | A09 LTO and PGO toolchain variants | Rejected | None | Independent five-pair tests improved the cumulative A06 median by 1.21% with LTO and 1.20% with PGO. Both preserved exact measured outputs and had positive paired intervals, but neither met the 2% practical gate. | | Q01 cumulative alignment stack | Release qualification | blackstar.2 | H01+A02+A05+A06 produced median paired improvements of 33.20% uncompressed and 28.54% through zcat across three balanced pairs, lowered RSS, preserved exact outputs, and passed shared-index, BAM, affinity, sanitizer, insertion, SAindex, upstream-compatibility, selector, and reproducible-package gates. | +| Q02 cross-workload generalization | Complete mixed Labs result | None | Nine public mode series passed compatibility and noninferiority. All 36/36 pair-level output comparisons passed, but the exclusive-node single-end aggregate exceeded the variability gate and does not support a speed claim. | +| TranscriptomeSAM primary selection | Unreleased hardening candidate | None | Official STAR primary flags changed between controlled 1- and 96-thread runs. BlackSTAR made the raw flags exact across thread counts while preserving transcript alignment sets, genomic BAM records, counts, junctions, and later inherited RNG stream position. | | Early 41 percent full-index claim | Superseded | None | Replaced by three-pair 49.48 percent release evidence. | | Early Delta build and runtime figures | Superseded | None | Replaced by hardened package and promotion-gate evidence. | diff --git a/docs/architecture/claims.tsv b/docs/architecture/claims.tsv index b5475268..9f426954 100644 --- a/docs/architecture/claims.tsv +++ b/docs/architecture/claims.tsv @@ -156,3 +156,23 @@ DIRECT-ALIGN-002 Released BlackSTAR zcat alignment median wall-time reduction at DIRECT-ALIGN-003 Released BlackSTAR uncompressed alignment reduction at 32 threads 2.3842 percent same public corpus and index; local SSD; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 threshold control; prevents generalizing high-thread gain to every thread count DIRECT-DELTA-001 Released BlackSTAR verified-cold Delta insertion speedup versus an official STAR full rebuild 31.4988 fold GRCh38 plus GFP and GST FASTA and insert-only GTF; zero resident input pages; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 39.04 versus 1225.62 seconds; mapping equivalence passed DIRECT-DELTA-002 Released BlackSTAR cold-to-warm Delta artifact comparisons 57/57 comparisons same named-sequence addition benchmark docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 all comparisons passed; GFP and GST each counted 100 fragments +LAB-Q02-001 Q02 current clean-candidate binary digest 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 sha256 ef2a256 archive-derived binary used for TranscriptomeSAM and exclusive single-end qualification docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c exact unreleased candidate identity; OpenMP linked +LAB-Q02-004 Q02 paired 76-base median paired wall-time improvement 20.0490 percent 12,768,316 public paired reads; GRCh38 and Ensembl 114; 96 threads; local storage; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility gate passed; positive interval; control CV exceeded separate 3 percent speed-claim preference +LAB-Q02-006 Q02 paired 150-base median paired wall-time improvement 12.6724 percent public fragmented paired 150-base RNA-seq; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all performance, variability, RSS, and correctness gates passed +LAB-Q02-008 Q02 exclusive-node single-end median paired result 1.6702 percent public single-end 150-base RNA-seq; 96 threads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c no speed claim; correctness and noninferiority passed but candidate CV 5.2171 percent failed the variability gate +LAB-Q02-010 Q02 two-pass median paired wall-time improvement 20.2527 percent full public paired 76-base corpus; Basic two-pass; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility gate passed; positive interval; control CV exceeded separate 3 percent speed-claim preference +LAB-Q02-012 Q02 BySJout median paired wall-time improvement 31.4513 percent full public paired 76-base corpus; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates and output comparisons passed +LAB-Q02-014 Q02 chimeric median paired wall-time improvement 31.7588 percent full public paired 76-base corpus; chimeric detection; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates, canonical chimeric records, junctions, counts, and metrics passed +LAB-Q02-016 Q02 coordinate-sorted BAM median paired wall-time improvement 18.4215 percent full public paired 76-base corpus; 96 threads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility and canonical BAM gates passed; control CV exceeded separate 3 percent speed-claim preference +LAB-Q02-017 Q02 TranscriptomeSAM upstream and candidate median wall times 217.33 / 213.58 seconds full public paired 76-base corpus; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c accepted noninferiority and lower RSS; interval crossed zero so no speed claim +LAB-Q02-019 Q02 repeated full-corpus candidate TranscriptomeSAM primary digest a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc sha256 warmup and three measured candidate runs at 96 threads docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c raw primary and secondary flags exact in 4/4 current-candidate runs +LAB-Q02-020 Q02 candidate TranscriptomeSAM primary flags exact across thread counts 1 and 96 threads public 250000-pair control under the same run seed docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c raw candidate primary digests identical +LAB-Q02-021 Q02 official STAR TranscriptomeSAM primary flags differed across thread counts 2/2 distinct digests same public 250000-pair control at 1 and 96 threads docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c controlled reproduction of inherited worker-dependent primary selection +LAB-Q02-023 Q02 STARsolo upstream and candidate median wall times 153.45 / 149.23 seconds public 10x Genomics v3 fixture; Gene feature; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 accepted noninferiority and exact output tree; interval crossed zero so no speed claim +LAB-Q02-026 Q02 STARlong median paired wall-time improvement 19.4807 percent public direct-RNA fixture; 96 threads; three order-balanced pairs; symmetric seedPerReadNmax 100000 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates and canonical SAM comparisons passed +LAB-Q02-027 Q02 mode-specific public pair correctness comparisons 36/36 pair comparisons ten public workload series including the nonaccepted single-end timing aggregate docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c every applicable metrics, junction, count, SAM, BAM, chimeric, or STARsolo oracle passed +LAB-Q02-028 Q02 current-candidate specialized compatibility matrix 12/12 checks fragmented paired, TranscriptomeSAM, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed-genome paths docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c all differential checks passed against official STAR where applicable +LAB-Q02-029 Q02 current-source focused sanitizer scripts 11/11 scripts affinity, NUMA, chunks, transcript primary, cgroup memory, packed array, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c all ASan and UBSan scripts passed +LAB-Q02-031 Q02 superseded shared-host single-end point estimate 22.5487 percent five-pair single-end series with 7.6146 and 6.4056 percent arm CV docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 rejected and superseded; must not be cited as a speed gain +LAB-Q02-032 Q02 32-thread single-end median paired result -1.9858 percent public single-end 150-base reads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 no gain and 2 percent noninferiority not established; prevents an all-thread-count claim +LAB-Q02-033 Q02 inherited STARlong default seed limit fail status public direct-RNA fixture under default seedPerReadNmax docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 both official and BlackSTAR STARlong abort; accepted performance comparison used symmetric 100000 override diff --git a/docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf b/docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf new file mode 100644 index 0000000000000000000000000000000000000000..dea7b3d94d498cf94e520d68616ea5174e283692 GIT binary patch literal 30636 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

r?ipEuB&~d13-9P5EtHEZEMbUHNb|PU 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 Uzs0QtM8c5?0`l@YCb|Ou10=P=WB>pF literal 0 HcmV?d00001 diff --git a/docs/architecture/diagrams/pdf/F28-transcriptome-primary-determinism.pdf b/docs/architecture/diagrams/pdf/F28-transcriptome-primary-determinism.pdf new file mode 100644 index 0000000000000000000000000000000000000000..9310862e6e81ec258895390c17ca05f361819353 GIT binary patch literal 24281 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 u`e^d`1g7E|PINNED CONTROL"] --> MATRIX["Q02 PUBLIC WORKLOAD MATRIX
96 threads; local storage; balanced pairs"] + CANDIDATE["UNRELEASED BLACKSTAR LABS CANDIDATE
blackstar.2 stack plus hardening"] --> MATRIX + + MATRIX --> BULK["FRAGMENTED AND SPECIALIZED BULK"] + BULK --> PE76["Paired 76-base
20.05% measured gain
5/5 outputs exact"] + BULK --> PE150["Paired 150-base
12.67% measured gain
3/3 outputs exact"] + BULK --> TWO["Two-pass
20.25% measured gain
3/3 outputs exact"] + BULK --> BYSJ["BySJout
31.45% measured gain
3/3 outputs exact"] + BULK --> CHIM["Chimeric
31.76% measured gain
3/3 outputs exact"] + BULK --> SORTED["Sorted BAM
18.42% measured gain
5/5 canonical BAM exact"] + + MATRIX --> OTHER["OTHER COMMON MODES"] + OTHER --> TX["TranscriptomeSAM
1.55% point estimate
accepted noninferiority"] + OTHER --> SOLO["STARsolo 10x v3
3.78% point estimate
accepted noninferiority"] + OTHER --> LONG["STARlong direct RNA
19.48% measured gain
3/3 canonical SAM exact"] + OTHER --> SINGLE["Single-end 150-base
1.67% point estimate
CV gate failed; no speed claim"] + + PE76 --> CORRECT["36/36 MODE-SPECIFIC
PAIR COMPARISONS PASSED"] + PE150 --> CORRECT + TWO --> CORRECT + BYSJ --> CORRECT + CHIM --> CORRECT + SORTED --> CORRECT + TX --> CORRECT + SOLO --> CORRECT + LONG --> CORRECT + SINGLE --> CORRECT + + CORRECT --> DECISION["MIXED Q02 DECISION
retain hardening candidate
outside release boundary"] + LIMIT["LIMITS
one CPU family; local storage; 96-thread primary series
STARlong requires symmetric seed-limit override
single-end timing remains unresolved"] -.-> DECISION + + classDef control fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef candidate fill:#dbeafe,stroke:#1d4ed8,color:#111827; + classDef group fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef gain fill:#ecfdf5,stroke:#047857,color:#111827; + classDef neutral fill:#fef3c7,stroke:#a16207,color:#111827; + classDef failed fill:#fee2e2,stroke:#b91c1c,color:#111827; + classDef decision fill:#dbeafe,stroke:#1d4ed8,color:#111827,stroke-width:3px; + class CONTROL control; + class CANDIDATE candidate; + class MATRIX,BULK,OTHER group; + class PE76,PE150,TWO,BYSJ,CHIM,SORTED,LONG,CORRECT gain; + class TX,SOLO,LIMIT neutral; + class SINGLE failed; + class DECISION decision; diff --git a/docs/architecture/diagrams/src/F28-transcriptome-primary-determinism.mmd b/docs/architecture/diagrams/src/F28-transcriptome-primary-determinism.mmd new file mode 100644 index 00000000..8f3e4b4c --- /dev/null +++ b/docs/architecture/diagrams/src/F28-transcriptome-primary-determinism.mmd @@ -0,0 +1,34 @@ +flowchart LR + READS["INPUT DISPATCH
stable global iReadAll ordinal"] --> UPWORK["OFFICIAL STAR WORKERS
worker-local RNG seeded by chunk"] + UPWORK --> UPSELECT["PRIMARY TRANSCRIPT CHOICE
depends on acquiring worker"] + UPSELECT --> UPTEST["CONTROLLED 1 VS 96 THREADS
raw primary digests differ"] + + READS --> BSTAR["BLACKSTAR SELECTOR
SplitMix64(runRNGseed, iReadAll)
modulo transcript alignment count"] + BSTAR --> BSELECT["PRIMARY TRANSCRIPT CHOICE
independent of worker scheduling"] + BSELECT --> BTEST["CONTROLLED 1 VS 96 THREADS
raw primary digests exact"] + + UPSELECT --> LEGACY["ONE LEGACY RNG DRAW RETAINED
later inherited RNG state does not shift"] + BSELECT --> LEGACY + + UPTEST --> ORACLE["BIOLOGICAL COMPATIBILITY ORACLE"] + BTEST --> ORACLE + LEGACY --> ORACLE + ORACLE --> ALIGN["Transcript alignment set exact
after clearing only SAM flag 0x100"] + ORACLE --> GENOME["Genomic BAM exact"] + ORACLE --> COUNTS["Gene counts and junctions exact"] + ALIGN --> RESULT["DETERMINISTIC COMPATIBILITY CORRECTION
alignment set preserved; visible primary flag stabilized"] + GENOME --> RESULT + COUNTS --> RESULT + + classDef input fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef upstream fill:#fee2e2,stroke:#b91c1c,color:#111827; + classDef candidate fill:#dbeafe,stroke:#1d4ed8,color:#111827; + classDef guard fill:#fef3c7,stroke:#a16207,color:#111827; + classDef passed fill:#ecfdf5,stroke:#047857,color:#111827; + classDef result fill:#dbeafe,stroke:#1d4ed8,color:#111827,stroke-width:3px; + class READS input; + class UPWORK,UPSELECT,UPTEST upstream; + class BSTAR,BSELECT,BTEST candidate; + class LEGACY guard; + class ORACLE,ALIGN,GENOME,COUNTS passed; + class RESULT result; diff --git a/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg b/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg new file mode 100644 index 00000000..40aa83df --- /dev/null +++ b/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg @@ -0,0 +1,2 @@ + +

OFFICIAL STAR 2.7.11b
PINNED CONTROL

Q02 PUBLIC WORKLOAD MATRIX
96 threads; local storage; balanced pairs

UNRELEASED BLACKSTAR LABS CANDIDATE
blackstar.2 stack plus hardening

FRAGMENTED AND SPECIALIZED BULK

Paired 76-base
20.05% measured gain
5/5 outputs exact

Paired 150-base
12.67% measured gain
3/3 outputs exact

Two-pass
20.25% measured gain
3/3 outputs exact

BySJout
31.45% measured gain
3/3 outputs exact

Chimeric
31.76% measured gain
3/3 outputs exact

Sorted BAM
18.42% measured gain
5/5 canonical BAM exact

OTHER COMMON MODES

TranscriptomeSAM
1.55% point estimate
accepted noninferiority

STARsolo 10x v3
3.78% point estimate
accepted noninferiority

STARlong direct RNA
19.48% measured gain
3/3 canonical SAM exact

Single-end 150-base
1.67% point estimate
CV gate failed; no speed claim

36/36 MODE-SPECIFIC
PAIR COMPARISONS PASSED

MIXED Q02 DECISION
retain hardening candidate
outside release boundary

LIMITS
one CPU family; local storage; 96-thread primary series
STARlong requires symmetric seed-limit override
single-end timing remains unresolved

diff --git a/docs/architecture/diagrams/svg/F28-transcriptome-primary-determinism.svg b/docs/architecture/diagrams/svg/F28-transcriptome-primary-determinism.svg new file mode 100644 index 00000000..8062bf6d --- /dev/null +++ b/docs/architecture/diagrams/svg/F28-transcriptome-primary-determinism.svg @@ -0,0 +1,2 @@ + +

INPUT DISPATCH
stable global iReadAll ordinal

OFFICIAL STAR WORKERS
worker-local RNG seeded by chunk

PRIMARY TRANSCRIPT CHOICE
depends on acquiring worker

CONTROLLED 1 VS 96 THREADS
raw primary digests differ

BLACKSTAR SELECTOR
SplitMix64(runRNGseed, iReadAll)
modulo transcript alignment count

PRIMARY TRANSCRIPT CHOICE
independent of worker scheduling

CONTROLLED 1 VS 96 THREADS
raw primary digests exact

ONE LEGACY RNG DRAW RETAINED
later inherited RNG state does not shift

BIOLOGICAL COMPATIBILITY ORACLE

Transcript alignment set exact
after clearing only SAM flag 0x100

Genomic BAM exact

Gene counts and junctions exact

DETERMINISTIC COMPATIBILITY CORRECTION
alignment set preserved; visible primary flag stabilized

diff --git a/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv b/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv new file mode 100644 index 00000000..5470202d --- /dev/null +++ b/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv @@ -0,0 +1,34 @@ +claim_id metric value unit scope qualification source_record +LAB-Q02-001 current_candidate_binary_sha256 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 sha256 Clean archive-derived candidate at ef2a256 used for TranscriptomeSAM and exclusive single-end qualification Exact candidate identity; OpenMP linked benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-002 phase_candidate_binary_sha256 fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489 sha256 Candidate used for paired, two-pass, BySJout, chimeric, sorted-BAM, and STARsolo series Current source changes after this binary affect TranscriptomeSAM selection and benchmark code, not these measured runtime paths benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-003 paired_76_upstream_and_candidate_median_wall_times 72.02 / 58.02 seconds 12,768,316 paired 76-base public reads; GRCh38 and Ensembl 114; 96 threads; local storage; five order-balanced pairs Compatibility gate passed; all five mode-specific output comparisons passed benchmarks/labs/Q02/20260726/results/paired76/result.json +LAB-Q02-004 paired_76_median_paired_improvement_and_interval 20.0490; 16.3245 to 23.9581 percent Same paired 76-base series Positive interval; upstream CV 4.0964 percent exceeds the separate 3 percent Labs speed-claim preference benchmarks/labs/Q02/20260726/results/paired76/result.json +LAB-Q02-005 paired_150_upstream_and_candidate_median_wall_times 89.77 / 78.66 seconds Public paired 150-base fragmented RNA-seq reads; common index; 96 threads; local storage; three order-balanced pairs Compatibility, variability, resource, correctness, and interval gates passed benchmarks/labs/Q02/20260726/results/paired150/result.json +LAB-Q02-006 paired_150_median_paired_improvement_and_interval 12.6724; 11.3846 to 13.4558 percent Same paired 150-base series Positive interval and both arm CV values below 1.5 percent benchmarks/labs/Q02/20260726/results/paired150/result.json +LAB-Q02-007 single_150_upstream_and_candidate_median_wall_times 61.52 / 58.95 seconds Public single-end 150-base reads; common index; 96 threads; dedicated node-local SSD; five order-balanced pairs Correctness, RSS, pair-count, and 2 percent noninferiority gates passed benchmarks/labs/Q02/20260726/results/single150/result.json +LAB-Q02-008 single_150_median_paired_improvement_and_interval 1.6702; -1.8743 to 13.4227 percent Same exclusive-node single-end series No speed claim; candidate CV 5.2171 percent exceeded the 5 percent compatibility threshold, so the aggregate result was not accepted benchmarks/labs/Q02/20260726/results/single150/result.json +LAB-Q02-009 two_pass_upstream_and_candidate_median_wall_times 223.91 / 177.05 seconds Full public paired 76-base corpus; Basic two-pass; 96 threads; three order-balanced pairs Compatibility gate and all three output comparisons passed benchmarks/labs/Q02/20260726/results/two-pass/result.json +LAB-Q02-010 two_pass_median_paired_improvement_and_interval 20.2527; 15.4366 to 20.9281 percent Same two-pass series Positive interval; upstream CV 4.4695 percent exceeds the separate 3 percent Labs speed-claim preference benchmarks/labs/Q02/20260726/results/two-pass/result.json +LAB-Q02-011 bysjout_upstream_and_candidate_median_wall_times 91.36 / 62.16 seconds Full public paired 76-base corpus; BySJout; 96 threads; three order-balanced pairs All gates and all three output comparisons passed benchmarks/labs/Q02/20260726/results/bysjout/result.json +LAB-Q02-012 bysjout_median_paired_improvement_and_interval 31.4513; 31.2172 to 35.6395 percent Same BySJout series Positive interval; both arm CV values below 3 percent benchmarks/labs/Q02/20260726/results/bysjout/result.json +LAB-Q02-013 chimeric_upstream_and_candidate_median_wall_times 89.55 / 60.09 seconds Full public paired 76-base corpus; chimeric detection; 96 threads; three order-balanced pairs All gates, canonical chimeric records, junctions, counts, and metrics passed benchmarks/labs/Q02/20260726/results/chimeric/result.json +LAB-Q02-014 chimeric_median_paired_improvement_and_interval 31.7588; 30.9786 to 34.0002 percent Same chimeric series Positive interval; both arm CV values below 2 percent benchmarks/labs/Q02/20260726/results/chimeric/result.json +LAB-Q02-015 sorted_bam_upstream_and_candidate_median_wall_times 106.56 / 85.31 seconds Full public paired 76-base corpus; coordinate-sorted BAM; 96 threads; five order-balanced pairs Compatibility gate and all five canonical BAM comparisons passed benchmarks/labs/Q02/20260726/results/sorted-bam/result.json +LAB-Q02-016 sorted_bam_median_paired_improvement_and_interval 18.4215; 16.9328 to 24.2640 percent Same five-pair sorted-BAM series Positive interval; upstream CV 3.7973 percent exceeds the separate 3 percent Labs speed-claim preference benchmarks/labs/Q02/20260726/results/sorted-bam/result.json +LAB-Q02-017 transcriptome_bam_upstream_and_candidate_median_wall_times 217.33 / 213.58 seconds Full public paired 76-base corpus; genomic and TranscriptomeSAM BAM; 96 threads; three order-balanced pairs Accepted noninferiority with 3.8239 percent lower candidate peak RSS; no speed claim benchmarks/labs/Q02/20260726/results/transcriptome/result.json +LAB-Q02-018 transcriptome_bam_median_paired_improvement_and_interval 1.5506; -0.4468 to 2.6291 percent Same TranscriptomeSAM series Interval crosses zero; all biological alignment sets, genomic BAMs, counts, junctions, and metrics matched benchmarks/labs/Q02/20260726/results/transcriptome/result.json +LAB-Q02-019 candidate_transcriptome_primary_digest a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc sha256 Full-corpus warmup and three measured current-candidate runs at 96 threads Exact raw primary and secondary flags across 4/4 candidate runs benchmarks/labs/Q02/20260726/results/transcriptome/result.json +LAB-Q02-020 candidate_transcriptome_thread_count_primary_digests f9f1417857e57087d5ffc260a7ff5ee5740c746a070b15acad842d85bb2c9091 / same sha256 Public 250,000-pair subset at 1 and 96 threads Candidate raw transcriptome records and primary flags were exact across thread counts benchmarks/labs/Q02/20260726/results/transcriptome-thread-invariance/result.json +LAB-Q02-021 upstream_transcriptome_thread_count_primary_digests 903a7e8b900ad928c3e90b3c50e89f4f5ed53ac93211402ec64d38f419e03827 / b6b772c7b04b1cca7b5de4b7e0eb4e3a314e3bd8e4c1d83fd0314fe15ccdbb86 sha256 Same public subset under official STAR at 1 and 96 threads Controlled reproduction of inherited worker-dependent primary-flag selection benchmarks/labs/Q02/20260726/results/transcriptome-thread-invariance/result.json +LAB-Q02-022 transcriptome_thread_count_alignment_set_digest 5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86 sha256 Official STAR and BlackSTAR at 1 and 96 threads after clearing only SAM flag 0x100 All four transcript alignment sets were exact; genomic BAM, counts, junctions, and metrics also matched benchmarks/labs/Q02/20260726/results/transcriptome-thread-invariance/result.json +LAB-Q02-023 starsolo_upstream_and_candidate_median_wall_times 153.45 / 149.23 seconds Public 10x Genomics v3 fixture; Gene feature; 96 threads; three order-balanced pairs Accepted noninferiority and exact STARsolo file tree; no speed claim benchmarks/labs/Q02/20260726/results/starsolo/result.json +LAB-Q02-024 starsolo_median_paired_improvement_and_interval 3.7800; -1.4337 to 10.4853 percent Same STARsolo series Interval crosses zero; superiority not established benchmarks/labs/Q02/20260726/results/starsolo/result.json +LAB-Q02-025 starlong_upstream_and_candidate_median_wall_times 72.79 / 58.61 seconds Public direct-RNA long-read fixture; 96 threads; three order-balanced pairs; seedPerReadNmax 100000 in both arms Compatibility, variability, resource, correctness, and interval gates passed benchmarks/labs/Q02/20260726/results/starlong/result.json +LAB-Q02-026 starlong_median_paired_improvement_and_interval 19.4807; 17.1073 to 22.3805 percent Same STARlong series Positive interval; canonical SAM records exact in all three pairs benchmarks/labs/Q02/20260726/results/starlong/result.json +LAB-Q02-027 mode_specific_pair_correctness 36/36 pair comparisons Ten public workload series including the nonaccepted single-end timing series Every pair passed its applicable metrics, junction, count, SAM, BAM, chimeric, or STARsolo output oracle benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-028 specialized_mode_compatibility_matrix 12/12 checks Synthetic fragmented paired, TranscriptomeSAM, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed-genome paths Current clean candidate matched the official STAR oracle where applicable benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-029 focused_sanitizer_matrix 11/11 scripts Affinity, NUMA, read chunks, transcriptome primary selection, cgroup memory, packed arrays, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 All focused ASan and UBSan scripts passed on the current source benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-030 initial_sorted_bam_series_variability 6.5935 / 1.6776 percent Initial three-pair coordinate-sorted BAM series Rejected for upstream-arm variability; replaced by the retained five-pair series benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-031 superseded_shared_host_single_end_improvement_and_cv 22.5487; 7.6146 / 6.4056 percent Five-pair single-end series on a shared host Rejected for both-arm variability and superseded by the exclusive-node result; must not be cited as a speed gain benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-032 single_end_32_thread_improvement_and_interval -1.9858; -4.0090 to 6.0157 percent Public single-end 150-base reads; 32 threads; five order-balanced pairs No gain and 2 percent noninferiority not established; prevents generalization across thread counts benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-033 starlong_default_seed_limit fail status Public direct-RNA fixture under inherited default seedPerReadNmax Both official STARlong and BlackSTAR STARlong abort; the accepted symmetric benchmark used 100000 benchmarks/labs/Q02/20260726/qualification-receipt.tsv diff --git a/docs/architecture/figures.json b/docs/architecture/figures.json index 1bfb18db..bdebab78 100644 --- a/docs/architecture/figures.json +++ b/docs/architecture/figures.json @@ -316,6 +316,30 @@ "caption": "BlackSTAR classifies issue origin, requires compatibility and correctness evidence, adds matched performance gates where needed, and publishes only reproducible protected-main release artifacts.", "alt_text": "Governance flow from issue, proposal, security report, or experiment through upstream-versus-BlackSTAR origin classification and a focused pull request. Required compiler, CodeQL, sanitizer, deterministic, and compatibility checks lead either directly to maintainer review or through matched performance and resource gates. Accepted work reaches protected main, two exact release builds, checksummed artifacts, SPDX SBOM and provenance, then an immutable semantic-version release. Prior release rollback and external deployment authorization remain separate boundaries.", "outputs": ["docs/architecture/diagrams/svg/F26-governance-release.svg", "docs/architecture/diagrams/pdf/F26-governance-release.pdf"] + }, + { + "id": "F27", + "title": "Q02 cross-workload generalization", + "maturity": "experimental", + "visibility": "public", + "code_commit": "ef2a2560013293a3cd93403d876f50a3d5ec759c", + "source": "docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd", + "evidence": ["docs/experiments/Q02-cross-workload-generalization.md", "docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv"], + "caption": "Q02 extends direct official-STAR comparisons across fragmented paired, single-end, two-pass, junction, chimeric, BAM, single-cell, transcriptome, and long-read modes while retaining the failed single-end timing gate.", + "alt_text": "Cross-workload qualification diagram comparing official STAR 2.7.11b with the unreleased BlackSTAR Labs candidate at 96 threads. Paired 76-base, paired 150-base, two-pass, BySJout, chimeric, coordinate-sorted BAM, and STARlong modes show positive measured intervals with every applicable output comparison passing. TranscriptomeSAM and STARsolo pass noninferiority without a speed claim. Single-end preserves outputs and noninferiority but fails its variability gate. Thirty-six of thirty-six mode-specific pair comparisons pass, leading to a mixed decision that retains the hardening candidate outside the release boundary.", + "outputs": ["docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg", "docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf"] + }, + { + "id": "F28", + "title": "Transcriptome primary determinism", + "maturity": "experimental", + "visibility": "public", + "code_commit": "ef2a2560013293a3cd93403d876f50a3d5ec759c", + "source": "docs/architecture/diagrams/src/F28-transcriptome-primary-determinism.mmd", + "evidence": ["docs/experiments/Q02-cross-workload-generalization.md", "docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv", "source/ReadAlign_quantTranscriptome.cpp", "source/TranscriptomePrimary.cpp"], + "caption": "BlackSTAR replaces worker-dependent TranscriptomeSAM primary selection with a run-seed and read-ordinal selector while retaining the legacy RNG draw and preserving the complete upstream transcript alignment set.", + "alt_text": "Before-and-after determinism diagram. Official STAR routes a stable input read ordinal to workers whose chunk-seeded local random generators choose transcript primary flags; a controlled one-versus-96-thread test produces different raw primary digests. BlackSTAR hashes the run seed and stable read ordinal with SplitMix64, making the choice independent of worker scheduling; the same test produces exact raw digests. One legacy random draw is retained to preserve later random-stream position. Transcript alignment sets after clearing only flag 0x100, genomic BAM records, gene counts, junctions, and timing-independent metrics remain exact.", + "outputs": ["docs/architecture/diagrams/svg/F28-transcriptome-primary-determinism.svg", "docs/architecture/diagrams/pdf/F28-transcriptome-primary-determinism.pdf"] } ] } diff --git a/docs/experiments/Q02-cross-workload-generalization.md b/docs/experiments/Q02-cross-workload-generalization.md new file mode 100644 index 00000000..95f24ed0 --- /dev/null +++ b/docs/experiments/Q02-cross-workload-generalization.md @@ -0,0 +1,227 @@ +# Q02: Cross-Workload Generalization + +## Status + +- State: complete; mixed qualification result +- Parent commit: + `0978542f0957c47548f5f00fc706e1e08c021375` +- Final experiment commit: + `ef2a2560013293a3cd93403d876f50a3d5ec759c` +- Opened: 2026-07-25 +- Decided: 2026-07-26 +- Release disposition: retained as an unreleased Labs hardening candidate + +Q02 asked whether BlackSTAR's improvements and compatibility controls extend +beyond the paired short-read, gene-count-only workload that originally drove +alignment optimization. It covers fragmented paired and single-end RNA-seq, +two-pass alignment, splice-junction filtering, chimeric detection, coordinate +sorting, transcriptome BAM output, STARsolo, STARlong, SAM input, WASP, +shared-memory lifecycle, and transformed-genome output. + +The result is deliberately mixed. Nine public performance series passed their +predeclared compatibility and noninferiority gates. The exclusive-node +single-end series preserved every measured output and passed noninferiority, +but failed its variability gate and does not support a speed claim. This record +retains that failure. + +## Hypothesis + +The released high-thread scheduling and NUMA changes should preserve inherited +STAR behavior across common workflows and produce either: + +1. a positive, replicated direct improvement over official STAR; or +2. a bounded noninferiority result with exact mode-specific outputs. + +The hardening added during Q02 should also close concrete liabilities found by +the audit without creating a new format or deployment dependency. + +## Scope + +Runtime and operational changes evaluated in this phase include: + +- record-safe fallback for SAM input when automatic FASTQ chunk sizing is + active; +- cgroup-aware host-memory assessment for parallel index strategies; +- restoration of inherited NUMA policy after private genome loading; +- isolated `STAR` and `STARlong` build state; +- explicit baseline x86-64 and AVX2 release variants; +- additional genome-insert annotation and identity checks; and +- deterministic `TranscriptomeSAM` primary-alignment selection based on the + run seed and stable input-read ordinal. + +The paired, two-pass, BySJout, chimeric, sorted-BAM, STARsolo, and STARlong +performance rows used the phase candidate binary identified by SHA-256 +`fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489`. +The final TranscriptomeSAM and exclusive single-end rows used the clean +archive-derived `ef2a256` binary identified by SHA-256 +`297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635`. +Changes between those binaries affect TranscriptomeSAM primary selection and +benchmark infrastructure, not the earlier measured FASTQ runtime paths. +Performance values nevertheless remain claims about the exact measured binary, +not an unmeasured future release artifact. + +This phase does not authorize a version bump, tag, GitHub update, release, or +external pipeline deployment. + +## Correctness Contract + +Each mode compares every artifact it can make biologically visible: + +| Mode | Required oracle | +| --- | --- | +| Paired, single, two-pass, BySJout | Timing-independent final metrics, sorted junctions, and gene counts | +| Chimeric | Core outputs plus canonical chimeric SAM records and junctions | +| Sorted BAM | Core outputs plus canonical coordinate-sorted BAM records | +| TranscriptomeSAM | Genomic BAM exact; transcript alignment set exact after clearing only flag `0x100`; BlackSTAR primary flags exact across repeated and cross-thread runs | +| STARsolo | Exact output inventory and exact file contents | +| STARlong | Canonical SAM records and timing-independent metrics | + +The specialized synthetic matrix additionally requires exact or +upstream-equivalent behavior for WASP, SAM input, cross-binary shared-memory +lifecycle, and haploid transformed-genome output. + +## Benchmark Contract + +- Official control: STAR 2.7.11b at + `b1edc1208d91a53bf40ebae8669f71d50b994851`. +- Index: GRCh38 with Ensembl 114 annotations. +- Primary paired corpus: 12,768,316 public paired 76-base ENCODE reads. +- Additional corpora: public paired and single-end 150-base fragmented RNA-seq, + public 10x Genomics v3, and public direct-RNA long reads. +- Hardware: hardware-matched dual-socket AMD EPYC 7742 systems; every series + stayed on one host. +- Storage: node-local SSD or NVMe; no measured path used network storage. +- Threads: 96 for primary public series; 1 and 96 for the transcriptome + determinism control. +- Design: seeded order-balanced pairs, excluded warmups immediately before + measurement, and three or five pairs per series. +- Compatibility gate: all correctness checks, at least three pairs, both arm + CV no greater than 5 percent, lower paired-bootstrap interval no worse than + -2 percent, and no more than 5 percent median peak-RSS growth. +- Speed reporting: noninferiority alone is never called a speedup. The interval + and variability remain visible beside every point estimate. + +Dedicated-node runs record an explicit quiet-gate waiver. Shared-host runs used +the quiet-system gate. A waiver does not relax correctness, order balancing, +replication, resource, or statistical requirements. + +## Public Performance Results + +Positive paired improvement means less BlackSTAR wall time. + +| Workload | Pairs | Official STAR | BlackSTAR | Median paired improvement | 95% interval | Peak-RSS change | Verdict | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | +| Paired 76-base fragmented | 5 | 72.02 s | 58.02 s | 20.05% | 16.32 to 23.96% | -5.68% | Compatibility pass; positive speed interval | +| Paired 150-base fragmented | 3 | 89.77 s | 78.66 s | 12.67% | 11.38 to 13.46% | -5.65% | All gates pass | +| Single-end 150-base | 5 | 61.52 s | 58.95 s | 1.67% | -1.87 to 13.42% | -5.66% | **Timing series fails CV; no speed claim** | +| Two-pass Basic | 3 | 223.91 s | 177.05 s | 20.25% | 15.44 to 20.93% | -8.76% | Compatibility pass; positive speed interval | +| BySJout | 3 | 91.36 s | 62.16 s | 31.45% | 31.22 to 35.64% | -4.21% | All gates pass | +| Chimeric detection | 3 | 89.55 s | 60.09 s | 31.76% | 30.98 to 34.00% | -5.68% | All gates pass | +| Coordinate-sorted BAM | 5 | 106.56 s | 85.31 s | 18.42% | 16.93 to 24.26% | -4.47% | Compatibility pass; canonical BAM exact | +| Genomic plus transcriptome BAM | 3 | 217.33 s | 213.58 s | 1.55% | -0.45 to 2.63% | -3.82% | Accepted noninferiority; no speed claim | +| STARsolo 10x v3 Gene | 3 | 153.45 s | 149.23 s | 3.78% | -1.43 to 10.49% | -4.83% | Accepted noninferiority; no speed claim | +| STARlong direct RNA | 3 | 72.79 s | 58.61 s | 19.48% | 17.11 to 22.38% | -2.22% | All gates pass with symmetric seed-limit override | + +The paired 76-base, two-pass, and sorted-BAM control arms had CV values between +3 and 5 percent. They pass the predeclared generalization compatibility gate, +but their measured gains should not be promoted as new release claims under the +separate Labs preference for less than 3 percent arm CV. Paired 150-base, +BySJout, chimeric, and STARlong satisfy that stricter variability preference. + +Across all ten series, including the nonaccepted single-end timing aggregate, +all 36/36 mode-specific pair comparisons passed. The single-end result therefore +identifies a performance-evidence limitation, not a correctness regression. + +## TranscriptomeSAM Liability and Fix + +Official STAR chooses one transcriptome primary alignment using an RNG owned by +the worker. Changing worker assignment can therefore change SAM flag `0x100` +without changing the alignment set. + +BlackSTAR now: + +1. retains one inherited RNG draw so later inherited random choices keep the + same stream position; +2. hashes `runRNGseed` and the stable `iReadAll` ordinal with SplitMix64; +3. selects the primary transcript modulo the number of transcript alignments; +4. leaves every transcript alignment and downstream count unchanged. + +The full public corpus produced raw candidate digest +`a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc` +in the warmup and all three measured runs. + +A controlled 250,000-pair test then used the same input at 1 and 96 threads: + +| Implementation | 1-thread primary digest | 96-thread primary digest | Result | +| --- | --- | --- | --- | +| Official STAR | `903a7e8b900a...` | `b6b772c7b04b...` | Different | +| BlackSTAR | `f9f1417857e5...` | `f9f1417857e5...` | Exact | + +All four transcript alignment sets had digest +`5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86` +after clearing only `0x100`. Genomic BAM records, gene counts, junctions, and +timing-independent metrics were also exact across all four runs. + +This is an intentional compatibility-visible correction: BlackSTAR preserves +the complete upstream transcript alignment set but may choose a different +primary transcript than one particular official STAR run. + +## Functional Gates + +The current clean candidate passed: + +- 11/11 focused ASan and UBSan scripts; +- the generalization benchmark-harness regression; +- 12/12 specialized-mode compatibility checks; +- a clean OpenMP-linked short-read build; and +- a clean isolated STARlong build and compatibility oracle. + +The specialized matrix includes fragmented paired alignment, TranscriptomeSAM +and deterministic primary flags, gene counts, two-pass, BySJout, chimeric, +WASP, STARsolo, SAM input, shared-memory cross-binary lifecycle, and +transformed-genome output. + +## Negative and Limiting Evidence + +- The first three-pair sorted-BAM series had 6.59 percent upstream CV and was + rejected. The retained five-pair rerun reduced maximum arm CV to 3.80 percent. +- A shared-host single-end series appeared 22.55 percent faster but had + 7.61/6.41 percent arm CV. It is superseded and must not be cited. +- The exclusive-node single-end rerun reduced that point estimate to 1.67 + percent and still narrowly failed the 5 percent variability gate. +- At 32 threads, the single-end point estimate was -1.99 percent with an + interval of -4.01 to +6.02 percent; even 2 percent noninferiority was not + established. +- Both official STARlong and BlackSTAR STARlong abort on the real direct-RNA + fixture under the inherited default `seedPerReadNmax`. The accepted + comparison used `--seedPerReadNmax 100000` in both arms. +- WASP, SAM-input, shared-memory, and transformed-genome paths have differential + functional coverage but no replicated performance claim. +- Results cover one CPU family, one GCC toolchain, local storage, one primary + high thread count, and the stated public fixtures. + +## Decision + +- Outcome: retain the hardening source and evidence as a Labs candidate. +- Compatibility: no measured biological-output regression across the tested + short-read, single-cell, specialized, and long-read modes. +- Performance: strong cumulative gains generalize to several fragmented and + specialized modes; STARsolo and TranscriptomeSAM are noninferior only. +- Unresolved: single-end performance remains variable and does not qualify as + an improvement. +- Release: not promoted, versioned, tagged, pushed, or deployed by Q02. +- Follow-up: any release candidate must rebuild from the eventual protected + commit and rerun release reproducibility plus the selected cumulative + workload matrix. + +## Plain-Language Takeaway + +BlackSTAR's multicore gains are not confined to one 3-prime gene-count +workflow. They remain substantial for paired fragmented reads, 150-base paired +reads, two-pass alignment, splice-junction filtering, chimeric detection, +sorted BAM output, and a real long-read fixture. STARsolo and transcriptome BAM +output remain compatible without a proven speed gain. Single-end output is +correct and noninferior in the exclusive-node test, but its timing is too +variable to claim improvement. BlackSTAR also removes an upstream +thread-dependent TranscriptomeSAM primary-flag behavior while preserving the +complete alignment set. diff --git a/docs/experiments/README.md b/docs/experiments/README.md index 482701aa..65bed6a5 100644 --- a/docs/experiments/README.md +++ b/docs/experiments/README.md @@ -57,6 +57,9 @@ claim an ordinary-path speedup. OpenMP binding accepted in `blackstar.2`. - [Q01](Q01-cumulative-alignment-qualification.md): cumulative H01+A02+A05+A06 release qualification supporting promotion into `blackstar.2`. +- [Q02](Q02-cross-workload-generalization.md): complete cross-workload + hardening and compatibility record; nine public series passed, while the + exclusive-node single-end timing series retained a variability-gate failure. ## Quiet-System Gate diff --git a/docs/experiments/ROADMAP.md b/docs/experiments/ROADMAP.md index 8b6b5d98..b9bdc4f7 100644 --- a/docs/experiments/ROADMAP.md +++ b/docs/experiments/ROADMAP.md @@ -21,6 +21,7 @@ until it passes cumulative qualification and is deliberately promoted. | A08 | Multi-sample scheduler | Shared immutable index memory plus explicit resource tokens improves node throughput with complete isolation. | Proposed | A07 | | A09 | Toolchain | LTO and profile-guided optimization improve the accepted cumulative alignment stack without semantic changes. | Rejected; LTO and PGO each gained about 1.2%, below the 2% practical gate | A06 | | Q01 | Cumulative alignment qualification | The complete H01+A02+A05+A06 stack preserves release behavior and generalizes across private, shared, compressed, BAM, affinity, sanitizer, compatibility, and package gates. | Complete; promoted in blackstar.2 | A06 and A09 decision | +| Q02 | Cross-workload generalization | The released high-thread stack and new compatibility hardening preserve behavior beyond paired gene-count-only RNA-seq. | Complete; nine public series passed, single-end timing remains unresolved, no release promotion | Q01 and successor transition | | I01 | Genome preparation | Parallel reverse-complement and bounded private prefix histograms reduce serial setup. | Proposed | Cumulative alignment qualification | | I02 | SA packing | Record-block partitioning permits deterministic disjoint-byte parallel packing. | Proposed | I01 | | I03 | Junction merge | Partitioned merge and rank calculation reduce the remaining serial junction stage. | Proposed | I02 | @@ -43,5 +44,11 @@ until it passes cumulative qualification and is deliberately promoted. - Q01 pre-promotion package artifacts retain the old `.1` source version and are not release assets. Only clean `.2` packages from the protected release commit are distributable. +- Q02 does not establish a single-end speed improvement. Its exclusive-node + five-pair series passed correctness, RSS, and noninferiority but exceeded the + 5 percent compatibility CV threshold. +- Q02 TranscriptomeSAM output intentionally stabilizes the primary transcript + flag across worker schedules. The complete upstream alignment set remains the + compatibility oracle after clearing only flag `0x100`. - I05 begins only after a documented I04 decision and retains the default v1 index format. From b00daceacd1c48f1d1b9f9b41fe876be5f978a58 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 18:59:07 +0000 Subject: [PATCH 14/22] Bank Q02 cross-workload benchmark receipts --- docs/PERFORMANCE.md | 20 ++++-- docs/architecture/claims.tsv | 40 +++++------ .../alignment-Q02-cross-workload-20260726.tsv | 66 +++++++++---------- .../Q02-cross-workload-20260726/README.md | 51 ++++++++++++++ .../Q02-cross-workload-20260726/SHA256SUMS | 32 +++++++++ .../qualification-receipt.tsv | 23 +++++++ .../results/bysjout/contract.json | 59 +++++++++++++++++ .../results/bysjout/pairs.tsv | 4 ++ .../results/bysjout/result.json | 26 ++++++++ .../results/chimeric/contract.json | 59 +++++++++++++++++ .../results/chimeric/pairs.tsv | 4 ++ .../results/chimeric/result.json | 26 ++++++++ .../results/paired150/contract.json | 58 ++++++++++++++++ .../results/paired150/pairs.tsv | 4 ++ .../results/paired150/result.json | 26 ++++++++ .../results/paired76/contract.json | 60 +++++++++++++++++ .../results/paired76/pairs.tsv | 6 ++ .../results/paired76/result.json | 26 ++++++++ .../results/single150/contract.json | 62 +++++++++++++++++ .../results/single150/pairs.tsv | 6 ++ .../results/single150/result.json | 27 ++++++++ .../results/sorted-bam/contract.json | 61 +++++++++++++++++ .../results/sorted-bam/pairs.tsv | 6 ++ .../results/sorted-bam/result.json | 26 ++++++++ .../results/starlong/contract.json | 59 +++++++++++++++++ .../results/starlong/pairs.tsv | 4 ++ .../results/starlong/result.json | 26 ++++++++ .../results/starsolo/contract.json | 59 +++++++++++++++++ .../results/starsolo/pairs.tsv | 4 ++ .../results/starsolo/result.json | 26 ++++++++ .../result.json | 45 +++++++++++++ .../results/transcriptome/contract.json | 60 +++++++++++++++++ .../results/transcriptome/pairs.tsv | 4 ++ .../results/transcriptome/result.json | 45 +++++++++++++ .../results/two-pass/contract.json | 59 +++++++++++++++++ .../results/two-pass/pairs.tsv | 4 ++ .../results/two-pass/result.json | 26 ++++++++ .../Q02-cross-workload-generalization.md | 12 ++++ docs/experiments/README.md | 11 +++- 39 files changed, 1161 insertions(+), 61 deletions(-) create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/README.md create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/SHA256SUMS create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/paired150/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/paired150/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/paired150/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/paired76/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/paired76/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/paired76/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/single150/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/single150/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/single150/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/starlong/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/starlong/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/starlong/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/contract.json create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/pairs.tsv create mode 100644 docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/result.json diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 1148440d..5440d7e7 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -114,14 +114,24 @@ noninferiority, but candidate CV was 5.22 percent. See [Q02](experiments/Q02-cross-workload-generalization.md) for exact binary identities, fixtures, resource results, negative evidence, and the -TranscriptomeSAM determinism correction. +TranscriptomeSAM determinism correction. Its bounded +[machine-readable receipts](benchmarks/Q02-cross-workload-20260726/README.md) +are tracked with an independent checksum manifest. ## Evidence -Bounded machine-readable receipts are committed under -`docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/`. Large raw outputs remain -outside Git. The receipts identify both binaries and preserve every published -aggregate needed to audit the claims. +Bounded machine-readable receipts are committed under: + +- `docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/` for the stable + cumulative comparison; and +- `docs/benchmarks/Q02-cross-workload-20260726/` for the unreleased + cross-workload Labs candidate. + +Large raw outputs remain outside Git. The receipts identify measured binaries +and inputs and preserve the pair data, aggregate values, negative evidence, and +gate decisions needed to audit the published claims. A retained absolute path +inside a benchmark contract records the original ephemeral run environment; it +is provenance, not a promise that the path exists in a fresh clone. ## Claim Rules diff --git a/docs/architecture/claims.tsv b/docs/architecture/claims.tsv index 9f426954..e3cb5c76 100644 --- a/docs/architecture/claims.tsv +++ b/docs/architecture/claims.tsv @@ -156,23 +156,23 @@ DIRECT-ALIGN-002 Released BlackSTAR zcat alignment median wall-time reduction at DIRECT-ALIGN-003 Released BlackSTAR uncompressed alignment reduction at 32 threads 2.3842 percent same public corpus and index; local SSD; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 threshold control; prevents generalizing high-thread gain to every thread count DIRECT-DELTA-001 Released BlackSTAR verified-cold Delta insertion speedup versus an official STAR full rebuild 31.4988 fold GRCh38 plus GFP and GST FASTA and insert-only GTF; zero resident input pages; three order-balanced pairs docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 39.04 versus 1225.62 seconds; mapping equivalence passed DIRECT-DELTA-002 Released BlackSTAR cold-to-warm Delta artifact comparisons 57/57 comparisons same named-sequence addition benchmark docs/architecture/evidence/direct-upstream-comparison-20260724.tsv b66f7341d620b559c4b6ab4c63dbd98ff1a7d5559e46371e5beec87fc7a3aa83 d6fbf932ae2b155ce4f689bce106429ab2bc07f6 all comparisons passed; GFP and GST each counted 100 fragments -LAB-Q02-001 Q02 current clean-candidate binary digest 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 sha256 ef2a256 archive-derived binary used for TranscriptomeSAM and exclusive single-end qualification docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c exact unreleased candidate identity; OpenMP linked -LAB-Q02-004 Q02 paired 76-base median paired wall-time improvement 20.0490 percent 12,768,316 public paired reads; GRCh38 and Ensembl 114; 96 threads; local storage; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility gate passed; positive interval; control CV exceeded separate 3 percent speed-claim preference -LAB-Q02-006 Q02 paired 150-base median paired wall-time improvement 12.6724 percent public fragmented paired 150-base RNA-seq; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all performance, variability, RSS, and correctness gates passed -LAB-Q02-008 Q02 exclusive-node single-end median paired result 1.6702 percent public single-end 150-base RNA-seq; 96 threads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c no speed claim; correctness and noninferiority passed but candidate CV 5.2171 percent failed the variability gate -LAB-Q02-010 Q02 two-pass median paired wall-time improvement 20.2527 percent full public paired 76-base corpus; Basic two-pass; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility gate passed; positive interval; control CV exceeded separate 3 percent speed-claim preference -LAB-Q02-012 Q02 BySJout median paired wall-time improvement 31.4513 percent full public paired 76-base corpus; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates and output comparisons passed -LAB-Q02-014 Q02 chimeric median paired wall-time improvement 31.7588 percent full public paired 76-base corpus; chimeric detection; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates, canonical chimeric records, junctions, counts, and metrics passed -LAB-Q02-016 Q02 coordinate-sorted BAM median paired wall-time improvement 18.4215 percent full public paired 76-base corpus; 96 threads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility and canonical BAM gates passed; control CV exceeded separate 3 percent speed-claim preference -LAB-Q02-017 Q02 TranscriptomeSAM upstream and candidate median wall times 217.33 / 213.58 seconds full public paired 76-base corpus; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c accepted noninferiority and lower RSS; interval crossed zero so no speed claim -LAB-Q02-019 Q02 repeated full-corpus candidate TranscriptomeSAM primary digest a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc sha256 warmup and three measured candidate runs at 96 threads docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c raw primary and secondary flags exact in 4/4 current-candidate runs -LAB-Q02-020 Q02 candidate TranscriptomeSAM primary flags exact across thread counts 1 and 96 threads public 250000-pair control under the same run seed docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c raw candidate primary digests identical -LAB-Q02-021 Q02 official STAR TranscriptomeSAM primary flags differed across thread counts 2/2 distinct digests same public 250000-pair control at 1 and 96 threads docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c controlled reproduction of inherited worker-dependent primary selection -LAB-Q02-023 Q02 STARsolo upstream and candidate median wall times 153.45 / 149.23 seconds public 10x Genomics v3 fixture; Gene feature; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 accepted noninferiority and exact output tree; interval crossed zero so no speed claim -LAB-Q02-026 Q02 STARlong median paired wall-time improvement 19.4807 percent public direct-RNA fixture; 96 threads; three order-balanced pairs; symmetric seedPerReadNmax 100000 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates and canonical SAM comparisons passed -LAB-Q02-027 Q02 mode-specific public pair correctness comparisons 36/36 pair comparisons ten public workload series including the nonaccepted single-end timing aggregate docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c every applicable metrics, junction, count, SAM, BAM, chimeric, or STARsolo oracle passed -LAB-Q02-028 Q02 current-candidate specialized compatibility matrix 12/12 checks fragmented paired, TranscriptomeSAM, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed-genome paths docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c all differential checks passed against official STAR where applicable -LAB-Q02-029 Q02 current-source focused sanitizer scripts 11/11 scripts affinity, NUMA, chunks, transcript primary, cgroup memory, packed array, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 ef2a2560013293a3cd93403d876f50a3d5ec759c all ASan and UBSan scripts passed -LAB-Q02-031 Q02 superseded shared-host single-end point estimate 22.5487 percent five-pair single-end series with 7.6146 and 6.4056 percent arm CV docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 rejected and superseded; must not be cited as a speed gain -LAB-Q02-032 Q02 32-thread single-end median paired result -1.9858 percent public single-end 150-base reads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 no gain and 2 percent noninferiority not established; prevents an all-thread-count claim -LAB-Q02-033 Q02 inherited STARlong default seed limit fail status public direct-RNA fixture under default seedPerReadNmax docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv fe10f8944f14cffce724eec62302b58d7b6f1e920e0e3398c4906a501fb5d608 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 both official and BlackSTAR STARlong abort; accepted performance comparison used symmetric 100000 override +LAB-Q02-001 Q02 current clean-candidate binary digest 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 sha256 ef2a256 archive-derived binary used for TranscriptomeSAM and exclusive single-end qualification docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c exact unreleased candidate identity; OpenMP linked +LAB-Q02-004 Q02 paired 76-base median paired wall-time improvement 20.0490 percent 12,768,316 public paired reads; GRCh38 and Ensembl 114; 96 threads; local storage; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility gate passed; positive interval; control CV exceeded separate 3 percent speed-claim preference +LAB-Q02-006 Q02 paired 150-base median paired wall-time improvement 12.6724 percent public fragmented paired 150-base RNA-seq; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all performance, variability, RSS, and correctness gates passed +LAB-Q02-008 Q02 exclusive-node single-end median paired result 1.6702 percent public single-end 150-base RNA-seq; 96 threads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c no speed claim; correctness and noninferiority passed but candidate CV 5.2171 percent failed the variability gate +LAB-Q02-010 Q02 two-pass median paired wall-time improvement 20.2527 percent full public paired 76-base corpus; Basic two-pass; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility gate passed; positive interval; control CV exceeded separate 3 percent speed-claim preference +LAB-Q02-012 Q02 BySJout median paired wall-time improvement 31.4513 percent full public paired 76-base corpus; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates and output comparisons passed +LAB-Q02-014 Q02 chimeric median paired wall-time improvement 31.7588 percent full public paired 76-base corpus; chimeric detection; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates, canonical chimeric records, junctions, counts, and metrics passed +LAB-Q02-016 Q02 coordinate-sorted BAM median paired wall-time improvement 18.4215 percent full public paired 76-base corpus; 96 threads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 compatibility and canonical BAM gates passed; control CV exceeded separate 3 percent speed-claim preference +LAB-Q02-017 Q02 TranscriptomeSAM upstream and candidate median wall times 217.33 / 213.58 seconds full public paired 76-base corpus; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c accepted noninferiority and lower RSS; interval crossed zero so no speed claim +LAB-Q02-019 Q02 repeated full-corpus candidate TranscriptomeSAM primary digest a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc sha256 warmup and three measured candidate runs at 96 threads docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c raw primary and secondary flags exact in 4/4 current-candidate runs +LAB-Q02-020 Q02 candidate TranscriptomeSAM primary flags exact across thread counts 1 and 96 threads public 250000-pair control under the same run seed docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c raw candidate primary digests identical +LAB-Q02-021 Q02 official STAR TranscriptomeSAM primary flags differed across thread counts 2/2 distinct digests same public 250000-pair control at 1 and 96 threads docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c controlled reproduction of inherited worker-dependent primary selection +LAB-Q02-023 Q02 STARsolo upstream and candidate median wall times 153.45 / 149.23 seconds public 10x Genomics v3 fixture; Gene feature; 96 threads; three order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 accepted noninferiority and exact output tree; interval crossed zero so no speed claim +LAB-Q02-026 Q02 STARlong median paired wall-time improvement 19.4807 percent public direct-RNA fixture; 96 threads; three order-balanced pairs; symmetric seedPerReadNmax 100000 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 all gates and canonical SAM comparisons passed +LAB-Q02-027 Q02 mode-specific public pair correctness comparisons 36/36 pair comparisons ten public workload series including the nonaccepted single-end timing aggregate docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c every applicable metrics, junction, count, SAM, BAM, chimeric, or STARsolo oracle passed +LAB-Q02-028 Q02 current-candidate specialized compatibility matrix 12/12 checks fragmented paired, TranscriptomeSAM, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed-genome paths docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c all differential checks passed against official STAR where applicable +LAB-Q02-029 Q02 current-source focused sanitizer scripts 11/11 scripts affinity, NUMA, chunks, transcript primary, cgroup memory, packed array, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 ef2a2560013293a3cd93403d876f50a3d5ec759c all ASan and UBSan scripts passed +LAB-Q02-031 Q02 superseded shared-host single-end point estimate 22.5487 percent five-pair single-end series with 7.6146 and 6.4056 percent arm CV docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 rejected and superseded; must not be cited as a speed gain +LAB-Q02-032 Q02 32-thread single-end median paired result -1.9858 percent public single-end 150-base reads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 no gain and 2 percent noninferiority not established; prevents an all-thread-count claim +LAB-Q02-033 Q02 inherited STARlong default seed limit fail status public direct-RNA fixture under default seedPerReadNmax docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 both official and BlackSTAR STARlong abort; accepted performance comparison used symmetric 100000 override diff --git a/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv b/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv index 5470202d..ae3033a3 100644 --- a/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv +++ b/docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv @@ -1,34 +1,34 @@ claim_id metric value unit scope qualification source_record -LAB-Q02-001 current_candidate_binary_sha256 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 sha256 Clean archive-derived candidate at ef2a256 used for TranscriptomeSAM and exclusive single-end qualification Exact candidate identity; OpenMP linked benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-002 phase_candidate_binary_sha256 fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489 sha256 Candidate used for paired, two-pass, BySJout, chimeric, sorted-BAM, and STARsolo series Current source changes after this binary affect TranscriptomeSAM selection and benchmark code, not these measured runtime paths benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-003 paired_76_upstream_and_candidate_median_wall_times 72.02 / 58.02 seconds 12,768,316 paired 76-base public reads; GRCh38 and Ensembl 114; 96 threads; local storage; five order-balanced pairs Compatibility gate passed; all five mode-specific output comparisons passed benchmarks/labs/Q02/20260726/results/paired76/result.json -LAB-Q02-004 paired_76_median_paired_improvement_and_interval 20.0490; 16.3245 to 23.9581 percent Same paired 76-base series Positive interval; upstream CV 4.0964 percent exceeds the separate 3 percent Labs speed-claim preference benchmarks/labs/Q02/20260726/results/paired76/result.json -LAB-Q02-005 paired_150_upstream_and_candidate_median_wall_times 89.77 / 78.66 seconds Public paired 150-base fragmented RNA-seq reads; common index; 96 threads; local storage; three order-balanced pairs Compatibility, variability, resource, correctness, and interval gates passed benchmarks/labs/Q02/20260726/results/paired150/result.json -LAB-Q02-006 paired_150_median_paired_improvement_and_interval 12.6724; 11.3846 to 13.4558 percent Same paired 150-base series Positive interval and both arm CV values below 1.5 percent benchmarks/labs/Q02/20260726/results/paired150/result.json -LAB-Q02-007 single_150_upstream_and_candidate_median_wall_times 61.52 / 58.95 seconds Public single-end 150-base reads; common index; 96 threads; dedicated node-local SSD; five order-balanced pairs Correctness, RSS, pair-count, and 2 percent noninferiority gates passed benchmarks/labs/Q02/20260726/results/single150/result.json -LAB-Q02-008 single_150_median_paired_improvement_and_interval 1.6702; -1.8743 to 13.4227 percent Same exclusive-node single-end series No speed claim; candidate CV 5.2171 percent exceeded the 5 percent compatibility threshold, so the aggregate result was not accepted benchmarks/labs/Q02/20260726/results/single150/result.json -LAB-Q02-009 two_pass_upstream_and_candidate_median_wall_times 223.91 / 177.05 seconds Full public paired 76-base corpus; Basic two-pass; 96 threads; three order-balanced pairs Compatibility gate and all three output comparisons passed benchmarks/labs/Q02/20260726/results/two-pass/result.json -LAB-Q02-010 two_pass_median_paired_improvement_and_interval 20.2527; 15.4366 to 20.9281 percent Same two-pass series Positive interval; upstream CV 4.4695 percent exceeds the separate 3 percent Labs speed-claim preference benchmarks/labs/Q02/20260726/results/two-pass/result.json -LAB-Q02-011 bysjout_upstream_and_candidate_median_wall_times 91.36 / 62.16 seconds Full public paired 76-base corpus; BySJout; 96 threads; three order-balanced pairs All gates and all three output comparisons passed benchmarks/labs/Q02/20260726/results/bysjout/result.json -LAB-Q02-012 bysjout_median_paired_improvement_and_interval 31.4513; 31.2172 to 35.6395 percent Same BySJout series Positive interval; both arm CV values below 3 percent benchmarks/labs/Q02/20260726/results/bysjout/result.json -LAB-Q02-013 chimeric_upstream_and_candidate_median_wall_times 89.55 / 60.09 seconds Full public paired 76-base corpus; chimeric detection; 96 threads; three order-balanced pairs All gates, canonical chimeric records, junctions, counts, and metrics passed benchmarks/labs/Q02/20260726/results/chimeric/result.json -LAB-Q02-014 chimeric_median_paired_improvement_and_interval 31.7588; 30.9786 to 34.0002 percent Same chimeric series Positive interval; both arm CV values below 2 percent benchmarks/labs/Q02/20260726/results/chimeric/result.json -LAB-Q02-015 sorted_bam_upstream_and_candidate_median_wall_times 106.56 / 85.31 seconds Full public paired 76-base corpus; coordinate-sorted BAM; 96 threads; five order-balanced pairs Compatibility gate and all five canonical BAM comparisons passed benchmarks/labs/Q02/20260726/results/sorted-bam/result.json -LAB-Q02-016 sorted_bam_median_paired_improvement_and_interval 18.4215; 16.9328 to 24.2640 percent Same five-pair sorted-BAM series Positive interval; upstream CV 3.7973 percent exceeds the separate 3 percent Labs speed-claim preference benchmarks/labs/Q02/20260726/results/sorted-bam/result.json -LAB-Q02-017 transcriptome_bam_upstream_and_candidate_median_wall_times 217.33 / 213.58 seconds Full public paired 76-base corpus; genomic and TranscriptomeSAM BAM; 96 threads; three order-balanced pairs Accepted noninferiority with 3.8239 percent lower candidate peak RSS; no speed claim benchmarks/labs/Q02/20260726/results/transcriptome/result.json -LAB-Q02-018 transcriptome_bam_median_paired_improvement_and_interval 1.5506; -0.4468 to 2.6291 percent Same TranscriptomeSAM series Interval crosses zero; all biological alignment sets, genomic BAMs, counts, junctions, and metrics matched benchmarks/labs/Q02/20260726/results/transcriptome/result.json -LAB-Q02-019 candidate_transcriptome_primary_digest a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc sha256 Full-corpus warmup and three measured current-candidate runs at 96 threads Exact raw primary and secondary flags across 4/4 candidate runs benchmarks/labs/Q02/20260726/results/transcriptome/result.json -LAB-Q02-020 candidate_transcriptome_thread_count_primary_digests f9f1417857e57087d5ffc260a7ff5ee5740c746a070b15acad842d85bb2c9091 / same sha256 Public 250,000-pair subset at 1 and 96 threads Candidate raw transcriptome records and primary flags were exact across thread counts benchmarks/labs/Q02/20260726/results/transcriptome-thread-invariance/result.json -LAB-Q02-021 upstream_transcriptome_thread_count_primary_digests 903a7e8b900ad928c3e90b3c50e89f4f5ed53ac93211402ec64d38f419e03827 / b6b772c7b04b1cca7b5de4b7e0eb4e3a314e3bd8e4c1d83fd0314fe15ccdbb86 sha256 Same public subset under official STAR at 1 and 96 threads Controlled reproduction of inherited worker-dependent primary-flag selection benchmarks/labs/Q02/20260726/results/transcriptome-thread-invariance/result.json -LAB-Q02-022 transcriptome_thread_count_alignment_set_digest 5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86 sha256 Official STAR and BlackSTAR at 1 and 96 threads after clearing only SAM flag 0x100 All four transcript alignment sets were exact; genomic BAM, counts, junctions, and metrics also matched benchmarks/labs/Q02/20260726/results/transcriptome-thread-invariance/result.json -LAB-Q02-023 starsolo_upstream_and_candidate_median_wall_times 153.45 / 149.23 seconds Public 10x Genomics v3 fixture; Gene feature; 96 threads; three order-balanced pairs Accepted noninferiority and exact STARsolo file tree; no speed claim benchmarks/labs/Q02/20260726/results/starsolo/result.json -LAB-Q02-024 starsolo_median_paired_improvement_and_interval 3.7800; -1.4337 to 10.4853 percent Same STARsolo series Interval crosses zero; superiority not established benchmarks/labs/Q02/20260726/results/starsolo/result.json -LAB-Q02-025 starlong_upstream_and_candidate_median_wall_times 72.79 / 58.61 seconds Public direct-RNA long-read fixture; 96 threads; three order-balanced pairs; seedPerReadNmax 100000 in both arms Compatibility, variability, resource, correctness, and interval gates passed benchmarks/labs/Q02/20260726/results/starlong/result.json -LAB-Q02-026 starlong_median_paired_improvement_and_interval 19.4807; 17.1073 to 22.3805 percent Same STARlong series Positive interval; canonical SAM records exact in all three pairs benchmarks/labs/Q02/20260726/results/starlong/result.json -LAB-Q02-027 mode_specific_pair_correctness 36/36 pair comparisons Ten public workload series including the nonaccepted single-end timing series Every pair passed its applicable metrics, junction, count, SAM, BAM, chimeric, or STARsolo output oracle benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-028 specialized_mode_compatibility_matrix 12/12 checks Synthetic fragmented paired, TranscriptomeSAM, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed-genome paths Current clean candidate matched the official STAR oracle where applicable benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-029 focused_sanitizer_matrix 11/11 scripts Affinity, NUMA, read chunks, transcriptome primary selection, cgroup memory, packed arrays, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 All focused ASan and UBSan scripts passed on the current source benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-030 initial_sorted_bam_series_variability 6.5935 / 1.6776 percent Initial three-pair coordinate-sorted BAM series Rejected for upstream-arm variability; replaced by the retained five-pair series benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-031 superseded_shared_host_single_end_improvement_and_cv 22.5487; 7.6146 / 6.4056 percent Five-pair single-end series on a shared host Rejected for both-arm variability and superseded by the exclusive-node result; must not be cited as a speed gain benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-032 single_end_32_thread_improvement_and_interval -1.9858; -4.0090 to 6.0157 percent Public single-end 150-base reads; 32 threads; five order-balanced pairs No gain and 2 percent noninferiority not established; prevents generalization across thread counts benchmarks/labs/Q02/20260726/qualification-receipt.tsv -LAB-Q02-033 starlong_default_seed_limit fail status Public direct-RNA fixture under inherited default seedPerReadNmax Both official STARlong and BlackSTAR STARlong abort; the accepted symmetric benchmark used 100000 benchmarks/labs/Q02/20260726/qualification-receipt.tsv +LAB-Q02-001 current_candidate_binary_sha256 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 sha256 Clean archive-derived candidate at ef2a256 used for TranscriptomeSAM and exclusive single-end qualification Exact candidate identity; OpenMP linked docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-002 phase_candidate_binary_sha256 fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489 sha256 Candidate used for paired, two-pass, BySJout, chimeric, sorted-BAM, and STARsolo series Current source changes after this binary affect TranscriptomeSAM selection and benchmark code, not these measured runtime paths docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-003 paired_76_upstream_and_candidate_median_wall_times 72.02 / 58.02 seconds 12,768,316 paired 76-base public reads; GRCh38 and Ensembl 114; 96 threads; local storage; five order-balanced pairs Compatibility gate passed; all five mode-specific output comparisons passed docs/benchmarks/Q02-cross-workload-20260726/results/paired76/result.json +LAB-Q02-004 paired_76_median_paired_improvement_and_interval 20.0490; 16.3245 to 23.9581 percent Same paired 76-base series Positive interval; upstream CV 4.0964 percent exceeds the separate 3 percent Labs speed-claim preference docs/benchmarks/Q02-cross-workload-20260726/results/paired76/result.json +LAB-Q02-005 paired_150_upstream_and_candidate_median_wall_times 89.77 / 78.66 seconds Public paired 150-base fragmented RNA-seq reads; common index; 96 threads; local storage; three order-balanced pairs Compatibility, variability, resource, correctness, and interval gates passed docs/benchmarks/Q02-cross-workload-20260726/results/paired150/result.json +LAB-Q02-006 paired_150_median_paired_improvement_and_interval 12.6724; 11.3846 to 13.4558 percent Same paired 150-base series Positive interval and both arm CV values below 1.5 percent docs/benchmarks/Q02-cross-workload-20260726/results/paired150/result.json +LAB-Q02-007 single_150_upstream_and_candidate_median_wall_times 61.52 / 58.95 seconds Public single-end 150-base reads; common index; 96 threads; dedicated node-local SSD; five order-balanced pairs Correctness, RSS, pair-count, and 2 percent noninferiority gates passed docs/benchmarks/Q02-cross-workload-20260726/results/single150/result.json +LAB-Q02-008 single_150_median_paired_improvement_and_interval 1.6702; -1.8743 to 13.4227 percent Same exclusive-node single-end series No speed claim; candidate CV 5.2171 percent exceeded the 5 percent compatibility threshold, so the aggregate result was not accepted docs/benchmarks/Q02-cross-workload-20260726/results/single150/result.json +LAB-Q02-009 two_pass_upstream_and_candidate_median_wall_times 223.91 / 177.05 seconds Full public paired 76-base corpus; Basic two-pass; 96 threads; three order-balanced pairs Compatibility gate and all three output comparisons passed docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/result.json +LAB-Q02-010 two_pass_median_paired_improvement_and_interval 20.2527; 15.4366 to 20.9281 percent Same two-pass series Positive interval; upstream CV 4.4695 percent exceeds the separate 3 percent Labs speed-claim preference docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/result.json +LAB-Q02-011 bysjout_upstream_and_candidate_median_wall_times 91.36 / 62.16 seconds Full public paired 76-base corpus; BySJout; 96 threads; three order-balanced pairs All gates and all three output comparisons passed docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/result.json +LAB-Q02-012 bysjout_median_paired_improvement_and_interval 31.4513; 31.2172 to 35.6395 percent Same BySJout series Positive interval; both arm CV values below 3 percent docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/result.json +LAB-Q02-013 chimeric_upstream_and_candidate_median_wall_times 89.55 / 60.09 seconds Full public paired 76-base corpus; chimeric detection; 96 threads; three order-balanced pairs All gates, canonical chimeric records, junctions, counts, and metrics passed docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/result.json +LAB-Q02-014 chimeric_median_paired_improvement_and_interval 31.7588; 30.9786 to 34.0002 percent Same chimeric series Positive interval; both arm CV values below 2 percent docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/result.json +LAB-Q02-015 sorted_bam_upstream_and_candidate_median_wall_times 106.56 / 85.31 seconds Full public paired 76-base corpus; coordinate-sorted BAM; 96 threads; five order-balanced pairs Compatibility gate and all five canonical BAM comparisons passed docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/result.json +LAB-Q02-016 sorted_bam_median_paired_improvement_and_interval 18.4215; 16.9328 to 24.2640 percent Same five-pair sorted-BAM series Positive interval; upstream CV 3.7973 percent exceeds the separate 3 percent Labs speed-claim preference docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/result.json +LAB-Q02-017 transcriptome_bam_upstream_and_candidate_median_wall_times 217.33 / 213.58 seconds Full public paired 76-base corpus; genomic and TranscriptomeSAM BAM; 96 threads; three order-balanced pairs Accepted noninferiority with 3.8239 percent lower candidate peak RSS; no speed claim docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json +LAB-Q02-018 transcriptome_bam_median_paired_improvement_and_interval 1.5506; -0.4468 to 2.6291 percent Same TranscriptomeSAM series Interval crosses zero; all biological alignment sets, genomic BAMs, counts, junctions, and metrics matched docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json +LAB-Q02-019 candidate_transcriptome_primary_digest a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc sha256 Full-corpus warmup and three measured current-candidate runs at 96 threads Exact raw primary and secondary flags across 4/4 candidate runs docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json +LAB-Q02-020 candidate_transcriptome_thread_count_primary_digests f9f1417857e57087d5ffc260a7ff5ee5740c746a070b15acad842d85bb2c9091 / same sha256 Public 250,000-pair subset at 1 and 96 threads Candidate raw transcriptome records and primary flags were exact across thread counts docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json +LAB-Q02-021 upstream_transcriptome_thread_count_primary_digests 903a7e8b900ad928c3e90b3c50e89f4f5ed53ac93211402ec64d38f419e03827 / b6b772c7b04b1cca7b5de4b7e0eb4e3a314e3bd8e4c1d83fd0314fe15ccdbb86 sha256 Same public subset under official STAR at 1 and 96 threads Controlled reproduction of inherited worker-dependent primary-flag selection docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json +LAB-Q02-022 transcriptome_thread_count_alignment_set_digest 5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86 sha256 Official STAR and BlackSTAR at 1 and 96 threads after clearing only SAM flag 0x100 All four transcript alignment sets were exact; genomic BAM, counts, junctions, and metrics also matched docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json +LAB-Q02-023 starsolo_upstream_and_candidate_median_wall_times 153.45 / 149.23 seconds Public 10x Genomics v3 fixture; Gene feature; 96 threads; three order-balanced pairs Accepted noninferiority and exact STARsolo file tree; no speed claim docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/result.json +LAB-Q02-024 starsolo_median_paired_improvement_and_interval 3.7800; -1.4337 to 10.4853 percent Same STARsolo series Interval crosses zero; superiority not established docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/result.json +LAB-Q02-025 starlong_upstream_and_candidate_median_wall_times 72.79 / 58.61 seconds Public direct-RNA long-read fixture; 96 threads; three order-balanced pairs; seedPerReadNmax 100000 in both arms Compatibility, variability, resource, correctness, and interval gates passed docs/benchmarks/Q02-cross-workload-20260726/results/starlong/result.json +LAB-Q02-026 starlong_median_paired_improvement_and_interval 19.4807; 17.1073 to 22.3805 percent Same STARlong series Positive interval; canonical SAM records exact in all three pairs docs/benchmarks/Q02-cross-workload-20260726/results/starlong/result.json +LAB-Q02-027 mode_specific_pair_correctness 36/36 pair comparisons Ten public workload series including the nonaccepted single-end timing series Every pair passed its applicable metrics, junction, count, SAM, BAM, chimeric, or STARsolo output oracle docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-028 specialized_mode_compatibility_matrix 12/12 checks Synthetic fragmented paired, TranscriptomeSAM, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed-genome paths Current clean candidate matched the official STAR oracle where applicable docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-029 focused_sanitizer_matrix 11/11 scripts Affinity, NUMA, read chunks, transcriptome primary selection, cgroup memory, packed arrays, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 All focused ASan and UBSan scripts passed on the current source docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-030 initial_sorted_bam_series_variability 6.5935 / 1.6776 percent Initial three-pair coordinate-sorted BAM series Rejected for upstream-arm variability; replaced by the retained five-pair series docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-031 superseded_shared_host_single_end_improvement_and_cv 22.5487; 7.6146 / 6.4056 percent Five-pair single-end series on a shared host Rejected for both-arm variability and superseded by the exclusive-node result; must not be cited as a speed gain docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-032 single_end_32_thread_improvement_and_interval -1.9858; -4.0090 to 6.0157 percent Public single-end 150-base reads; 32 threads; five order-balanced pairs No gain and 2 percent noninferiority not established; prevents generalization across thread counts docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv +LAB-Q02-033 starlong_default_seed_limit fail status Public direct-RNA fixture under inherited default seedPerReadNmax Both official STARlong and BlackSTAR STARlong abort; the accepted symmetric benchmark used 100000 docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv diff --git a/docs/benchmarks/Q02-cross-workload-20260726/README.md b/docs/benchmarks/Q02-cross-workload-20260726/README.md new file mode 100644 index 00000000..b1980f93 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/README.md @@ -0,0 +1,51 @@ +# Q02 Cross-Workload Qualification Receipts + +This directory preserves the bounded, machine-readable evidence behind +[Q02](../../experiments/Q02-cross-workload-generalization.md) and the +[Q02 evidence ledger](../../architecture/evidence/alignment-Q02-cross-workload-20260726.tsv). +Q02 compared official STAR 2.7.11b with an unreleased BlackSTAR hardening +candidate across ten public workload series. + +## Contents + +- `qualification-receipt.tsv` records source commits, executable identities, + toolchain and host context, functional gates, limiting evidence, and the + unreleased disposition. +- `results//contract.json` records the predeclared benchmark inputs, + executable and input hashes, run order, resource limits, and tool identities. +- `results//pairs.tsv` preserves each measured order-balanced pair. +- `results//result.json` preserves the aggregate statistics and gate + decisions. +- `results/transcriptome-thread-invariance/result.json` preserves the separate + 1-versus-96-thread TranscriptomeSAM determinism check. +- `SHA256SUMS` authenticates every retained receipt other than the manifest + itself. + +## Evidence Boundary + +These receipts are sufficient to audit the published aggregate values, +acceptance decisions, negative evidence, and exact identities of the measured +inputs and executables. The recorded absolute paths describe the original +ephemeral benchmark environment and are retained as provenance; they are not +expected to resolve in a fresh clone. + +Raw FASTQs, genome indexes, binaries, logs, BAMs, STARsolo output trees, and +other large generated artifacts are intentionally excluded from Git. Their +content identities remain in the contracts. The Q02 harness under +`benchmarks/generalization/` provides the runnable procedure, but reproducing +the measurements requires reacquiring the identified public fixtures and +building the identified source commits. + +## Candidate Boundary + +Most runtime series used the phase-candidate executable with SHA-256 +`fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489`. +TranscriptomeSAM and exclusive-node single-end qualification used the later +clean archive-derived executable with SHA-256 +`297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635`. +The distinction is preserved in the qualification receipt and individual +contracts. + +Q02 did not authorize a release, version bump, GitHub update, or external +deployment. Its results describe the exact measured binaries, not an +unmeasured future release artifact. diff --git a/docs/benchmarks/Q02-cross-workload-20260726/SHA256SUMS b/docs/benchmarks/Q02-cross-workload-20260726/SHA256SUMS new file mode 100644 index 00000000..f1539aec --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/SHA256SUMS @@ -0,0 +1,32 @@ +455b28168ff96c3742f9f9489ce4f7b23b0b0f0715fe539321069358e9414c65 qualification-receipt.tsv +bfb6201600fafb25bc7c7ffce68a7019d1cd28b9e904cac40fd16179bebbfb8a results/bysjout/contract.json +2fde7968571d2c42533d5ef548ab767eab68a5a8dd414b2cf6ec9c5d0760f7ba results/bysjout/pairs.tsv +fe36c121ef22a2c220b607dc9bd5a4864c022693d0e819a27e280cc87e77ef80 results/bysjout/result.json +af503c9098f52165484ad008a9cd0b01ca62efd6d9ad2b6de2671d0334aec0d0 results/chimeric/contract.json +9862145821d6faf76c7372f84f6379eb3f3fac9a64cfdd3e9fd1f1cd367345ad results/chimeric/pairs.tsv +ef472aefbd015bb22792a3c9253c1588759d5a2d4cd594f205346575e29312d9 results/chimeric/result.json +686aae1f4c971d99dac1bf8f3f11659eec707c63fdaf45749e9ecdd32f1d0cc8 results/paired150/contract.json +fc99a31322c62b8aacc2c3d8483898789656c798524d68b9bb34d8d1b08a3789 results/paired150/pairs.tsv +d2d1de782f4edf6456fa420e8c11743c65ee93ebc5b141310eb5051850e42479 results/paired150/result.json +9b91077938d59b7983efdebfbb185aace9fbbc756db28caf8b22f9fbd60404f5 results/paired76/contract.json +ee5cbd1db91a1000f0b2146e02bdbe20108d18126cdc3021cf5a129ab67ba666 results/paired76/pairs.tsv +ddf2882448441956b4e844a0a38aecac7abe091c5c76b79a2432d80ca70d7c78 results/paired76/result.json +979f0c243742876697d1b0245244b63e5f8a2aae4272453df2e35a952a148ba8 results/single150/contract.json +4210c844c01d01658b957d09b8d54bdff86b5934929e2a985f435a360efcd926 results/single150/pairs.tsv +38e361d3c9124c84d2109326fa5ad8eca2bace8b9b72994b445719e7428249b7 results/single150/result.json +552c8516d9b31dff3ceb5857dfa5c2808bce41382156a2e59392c09a46f66c70 results/sorted-bam/contract.json +ff491b4727b567edaa469aa3d8d7dffd25aa3d91cbf28e94d66a9e0773f4006d results/sorted-bam/pairs.tsv +a2a422de727f94e61c925d6bfae25852245ffdb39e543a0091e66dc8e6fa58f8 results/sorted-bam/result.json +13061b9bd8e4ddfce9bf41e19fc5a8f3acf0a4be067cd793caadb048c7bc8a1d results/starlong/contract.json +cc4fb0b4b5e15077c0e736b826e0e914c696f243e327d97e239ffb0459916859 results/starlong/pairs.tsv +4d36d0b481188a0d6e8a41f21a07cc43aee1b2c09a514ee847350cd306030da0 results/starlong/result.json +97622dfbea2a9d40b78b59333dec286f4dd37c821f2a7d3547400c3c041d50d0 results/starsolo/contract.json +14da1d72e945efbe4d439892308f3215dc6d3865576dc7b5acb089e3170c0023 results/starsolo/pairs.tsv +44335959b654f455a4a56c9bb54fc1122e61e8008a441156c1d41d0bfb75f343 results/starsolo/result.json +11bb109444ffddc8dda942345641baae6152f34f85c35e29cd59f7e620532a86 results/transcriptome-thread-invariance/result.json +4039416135e626db29e672bcb299e4662460e73a3a0be0ee89d475e9160501ce results/transcriptome/contract.json +f61142fa9236dc529d2b4c2ebfdb43796d2e558938c6d54f75bf9f1088116100 results/transcriptome/pairs.tsv +f204667f230ab0d8baf677f1568a348da8bd759367ae67c2b19345f193e44ca4 results/transcriptome/result.json +ff04ed62289c412c73e7fbeab54fd5698c8cd4a004d1bfa1b172dec3e59d6131 results/two-pass/contract.json +f7e2f57c65da795e46b8fc4ec5b7abef43f0ed84b54e1d4c04385999a1c63821 results/two-pass/pairs.tsv +3f68f4ba177a49dad7b1605fcc39681b2f608d7984c8296567454a8be026271b results/two-pass/result.json diff --git a/docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv b/docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv new file mode 100644 index 00000000..3254eee4 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/qualification-receipt.tsv @@ -0,0 +1,23 @@ +field value qualification source +schema blackstar-q02-qualification-v1 exact qualification procedure +created_utc 2026-07-26T04:23:01Z recorded after all benchmark jobs completed system clock +upstream_source_commit b1edc1208d91a53bf40ebae8669f71d50b994851 official STAR 2.7.11b compatibility oracle git +candidate_source_commit ef2a2560013293a3cd93403d876f50a3d5ec759c clean archive-derived current candidate git +upstream_short_read_binary_sha256 11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85 exact binary identity benchmark contracts +phase_short_read_binary_sha256 fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489 exact binary for paired, two-pass, BySJout, chimeric, sorted-BAM, and STARsolo series benchmark contracts +current_short_read_binary_sha256 297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635 exact ef2a256 binary for TranscriptomeSAM and single-end series benchmark contracts +upstream_starlong_binary_sha256 9d990982fbda6c6a6fc6615a194e4652dca0cf1d1d582afdc7e3d2ab7cddca3e exact STARlong control binary benchmark contract +candidate_starlong_binary_sha256 0f99f0668dd1fc3d8d3086ff9b7af1d2f90afe80d9dcbb0126ce9f74e7088a62 exact BlackSTAR STARlong binary benchmark contract +genome_parameters_sha256 9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19 common GRCh38 and Ensembl 114 index benchmark contracts +host_topology 2 sockets; 64 cores per socket; 2 threads per core; 8 NUMA nodes hardware-matched AMD EPYC 7742 hosts; each series stayed on one host lscpu +toolchain GCC 13.3.0; samtools 1.19.2; Linux 6.8 x86-64 local builds run provenance +storage node-local SSD or NVMe no network filesystem in measured paths run provenance +threads 96 primary performance series benchmark contracts +quiet_gate dedicated-host waiver or passed shared-host gate waiver recorded per contract benchmark contracts +clean_archive_build pass STAR 2.7.11b-blackstar.3; OpenMP linked clean build inspection +focused_asan_ubsan 11/11 affinity; NUMA; chunks; transcriptome primary; system memory; packed array; suffix comparator; transcript initialization; parameters; junctions; SHA-256 local and clean-candidate test runs +generalization_harness pass synthetic driver, comparator, and determinism regression extras/tests/scripts/testGeneralizationHarness.sh +specialized_mode_matrix 12/12 fragmented paired; transcriptome BAM and primary determinism; counts; two-pass; BySJout; chimeric; WASP; STARsolo; SAM input; shared lifecycle; transform extras/tests/scripts/testSpecializedModes.sh +single_end_performance_gate fail correctness, pair count, RSS, and noninferiority passed; candidate CV 5.2171 percent exceeded 5 percent results/single150/result.json +transcriptome_thread_invariance pass candidate raw primary flags exact at 1 and 96 threads; upstream raw primary flags differed results/transcriptome-thread-invariance/result.json +release_status unreleased no version bump, promotion, tag, GitHub push, or external deployment authorized project boundary diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/contract.json new file mode 100644 index 00000000..616ae330 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/contract.json @@ -0,0 +1,59 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-26T00:59:53.650795Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "bysjout", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": true, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "d238aa754f877e275a70b1bb4cb038629c263e1bdb7bc479c0c5a2682b65c9b2", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-a00-public/ENCFF000CXW.first-250000.fastq.gz", + "warmup_read1_sha256": "ff82aa39a6364e0ef135f77bcc478bcea872afb69471af79f403e72a9179d248", + "warmup_read2": "/tmp/blackstar-a00-public/ENCFF000CYM.first-250000.fastq.gz", + "warmup_read2_sha256": "6507ffb562a348c43323b1a0b06ad882bf8f6f6bb5462b5eb8dbe5e128ba0785" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/pairs.tsv new file mode 100644 index 00000000..61bbd456 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 90.68 62.16 31.451257168063528 42508288 40720384 True +2 BA 91.36 62.84 31.217162872154113 42512384 40714240 True +3 AB 92.65 59.63 35.639503507825154 42504192 40726528 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/result.json new file mode 100644 index 00000000..90a4cdd6 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/bysjout/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 1.0928135097509264, + "bootstrap_95_ci_percent": [ + 31.217162872154113, + 35.639503507825154 + ], + "candidate_cv_percent": 2.748499251264262, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 91.36, + "median_candidate_wall_seconds": 62.16, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 31.451257168063528, + "median_rss_increase_percent": -4.206012719213722, + "mode": "bysjout", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/contract.json new file mode 100644 index 00000000..5872ffc4 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/contract.json @@ -0,0 +1,59 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-26T01:09:58.642035Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "chimeric", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": true, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "d238aa754f877e275a70b1bb4cb038629c263e1bdb7bc479c0c5a2682b65c9b2", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-a00-public/ENCFF000CXW.first-250000.fastq.gz", + "warmup_read1_sha256": "ff82aa39a6364e0ef135f77bcc478bcea872afb69471af79f403e72a9179d248", + "warmup_read2": "/tmp/blackstar-a00-public/ENCFF000CYM.first-250000.fastq.gz", + "warmup_read2_sha256": "6507ffb562a348c43323b1a0b06ad882bf8f6f6bb5462b5eb8dbe5e128ba0785" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/pairs.tsv new file mode 100644 index 00000000..4dead862 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 87.06 60.09 30.978635423845624 39915520 37648384 True +2 BA 90.47 59.71 34.000221067757266 39915520 37662720 True +3 AB 89.55 61.11 31.758793969849247 39911424 37648384 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/result.json new file mode 100644 index 00000000..9c72ed68 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/chimeric/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 1.9816635266547666, + "bootstrap_95_ci_percent": [ + 30.978635423845624, + 34.000221067757266 + ], + "candidate_cv_percent": 1.2005481180758533, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 89.55, + "median_candidate_wall_seconds": 60.09, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 31.758793969849247, + "median_rss_increase_percent": -5.679835813237558, + "mode": "chimeric", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/contract.json new file mode 100644 index 00000000..b3f77b64 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/contract.json @@ -0,0 +1,58 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-25T22:11:41.910803Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "paired-mapping", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": false, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk150/SRR23992141_1.fastq", + "read1_sha256": "cd5d6d8ca4bd23dbd8c13efe1b02b30ca1143e125c66cb1a3bd56bee70b1c191", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk150/SRR23992141_2.fastq", + "read2_sha256": "cef190e1605779d65966a328d7369d509965c4ae9bc9d4f8b05d16b805d249ae", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "f1695024a74458a6264c59571b24497f116c3c686951a9e85bd9cbe8b6855e46", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_read1": "/tmp/blackstar-generalization-20260725/data/bulk150/SRR23992141_1.first-250000.fastq.gz", + "warmup_read1_sha256": "b3013050ee42045df3bf880b4b1477720cbd014a5402d0da8134427c99b213dc", + "warmup_read2": "/tmp/blackstar-generalization-20260725/data/bulk150/SRR23992141_2.first-250000.fastq.gz", + "warmup_read2_sha256": "4a195a5437489788c340e22dc35f0a67ce4afb43b57270a594d27a901578480e" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/pairs.tsv new file mode 100644 index 00000000..fab9c282 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 89.77 79.55 11.384649660242841 40212884 37942540 True +2 BA 90.89 78.66 13.455825723401919 40224572 37953656 True +3 AB 88.46000000000001 77.25 12.672394302509618 40226404 37953180 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/result.json new file mode 100644 index 00000000..2fc771fd --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/paired150/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 1.3557937357128205, + "bootstrap_95_ci_percent": [ + 11.384649660242841, + 13.455825723401919 + ], + "candidate_cv_percent": 1.4776468037956323, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 89.77, + "median_candidate_wall_seconds": 78.66, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 12.672394302509618, + "median_rss_increase_percent": -5.646777298214634, + "mode": "paired-mapping", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/contract.json new file mode 100644 index 00000000..51fe9422 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/contract.json @@ -0,0 +1,60 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-25T21:52:39.569489Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "paired-mapping", + "orders": [ + "AB", + "BA", + "AB", + "BA", + "AB" + ], + "pairs": 5, + "quiet_duration": 300.0, + "quiet_gate_skipped": false, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "f1695024a74458a6264c59571b24497f116c3c686951a9e85bd9cbe8b6855e46", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_read1": "/tmp/blackstar-a00-public/ENCFF000CXW.first-250000.fastq.gz", + "warmup_read1_sha256": "ff82aa39a6364e0ef135f77bcc478bcea872afb69471af79f403e72a9179d248", + "warmup_read2": "/tmp/blackstar-a00-public/ENCFF000CYM.first-250000.fastq.gz", + "warmup_read2_sha256": "6507ffb562a348c43323b1a0b06ad882bf8f6f6bb5462b5eb8dbe5e128ba0785" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/pairs.tsv new file mode 100644 index 00000000..a0e2d31e --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/pairs.tsv @@ -0,0 +1,6 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 69.03999999999999 57.13 17.25086906141366 39921664 37644288 True +2 BA 72.02 57.38 20.327686753679526 39917568 37648384 True +3 AB 69.65 58.28 16.32447954055995 39913472 37654528 True +4 BA 76.3 58.02 23.95806028833551 39917568 37648384 True +5 AB 73.47 58.74 20.048999591670068 39911424 37646336 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/result.json new file mode 100644 index 00000000..64a940b5 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/paired76/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 4.096425744115452, + "bootstrap_95_ci_percent": [ + 16.32447954055995, + 23.95806028833551 + ], + "candidate_cv_percent": 1.1347175034326817, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 72.02, + "median_candidate_wall_seconds": 58.02, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 20.048999591670068, + "median_rss_increase_percent": -5.684674978195065, + "mode": "paired-mapping", + "pair_count": 5, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/single150/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/single150/contract.json new file mode 100644 index 00000000..3499f379 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/single150/contract.json @@ -0,0 +1,62 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-deterministic-build-20260726/source/STAR", + "candidate_sha256": "297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635", + "created_utc": "2026-07-26T04:06:18.000619Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "single-mapping", + "orders": [ + "AB", + "BA", + "AB", + "BA", + "AB" + ], + "pairs": 5, + "quiet_duration": 300.0, + "quiet_gate_skipped": true, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk150/SRR23992141_1.fastq", + "read1_sha256": "cd5d6d8ca4bd23dbd8c13efe1b02b30ca1143e125c66cb1a3bd56bee70b1c191", + "read2": "none", + "read2_sha256": "none", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "4f7cd49309e03a573ed68b863569a8318eb42e2268b4a3641a380ce5fcee8c2e", + "pair_driver_sha256": "451bc8fee44053a48741eeeb64e1e6c6d81e0156782c95a6647aec935550d662", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "transcriptome_primary_oracle": "not-applicable", + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-generalization-20260725/data/bulk150/SRR23992141_1.fastq", + "warmup_read1_sha256": "cd5d6d8ca4bd23dbd8c13efe1b02b30ca1143e125c66cb1a3bd56bee70b1c191", + "warmup_read2": "none", + "warmup_read2_sha256": "none" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/single150/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/single150/pairs.tsv new file mode 100644 index 00000000..175a1fb8 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/single150/pairs.tsv @@ -0,0 +1,6 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 61.07 60.05 1.6702145079417114 40030364 37771180 True +2 BA 61.91 53.6 13.422710386044251 40029748 37823784 True +3 AB 63.16 58.95 6.665611146295114 40030848 37761144 True +4 BA 57.62 58.7 -1.8743491843110125 40066516 37760564 True +5 AB 61.52 61.79 -0.4388816644993433 40035208 37763960 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/single150/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/single150/result.json new file mode 100644 index 00000000..bd80ce25 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/single150/result.json @@ -0,0 +1,27 @@ +{ + "accepted": false, + "baseline_cv_percent": 3.394098293145235, + "bootstrap_95_ci_percent": [ + -1.8743491843110125, + 13.422710386044251 + ], + "candidate_cv_percent": 5.217108195723158, + "candidate_transcriptome_primary_digests": [], + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": false, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 61.52, + "median_candidate_wall_seconds": 58.95, + "median_gain_at_least_2_percent": false, + "median_improvement_percent": 1.6702145079417114, + "median_rss_increase_percent": -5.662852807914536, + "mode": "single-mapping", + "pair_count": 5, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": false +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/contract.json new file mode 100644 index 00000000..8afe2225 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/contract.json @@ -0,0 +1,61 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-26T01:41:07.120040Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "sorted-bam", + "orders": [ + "AB", + "BA", + "AB", + "BA", + "AB" + ], + "pairs": 5, + "quiet_duration": 300.0, + "quiet_gate_skipped": true, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "d238aa754f877e275a70b1bb4cb038629c263e1bdb7bc479c0c5a2682b65c9b2", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "warmup_read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "warmup_read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "warmup_read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/pairs.tsv new file mode 100644 index 00000000..8c5868d3 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/pairs.tsv @@ -0,0 +1,6 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 112.43 85.15 24.26398648047674 44377124 42528432 True +2 BA 110.24000000000001 85.06 22.84107402031931 44422664 42436408 True +3 AB 104.19 86.08 17.381706497744506 44518644 42436236 True +4 BA 102.7 85.31 16.932814021421617 44528180 42526532 True +5 AB 106.56 86.93 18.42154654654654 44587132 42538864 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/result.json new file mode 100644 index 00000000..d31afb34 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/sorted-bam/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 3.797273131235914, + "bootstrap_95_ci_percent": [ + 16.932814021421617, + 24.26398648047674 + ], + "candidate_cv_percent": 0.926345001008353, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 106.56, + "median_candidate_wall_seconds": 85.31, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 18.42154654654654, + "median_rss_increase_percent": -4.47478139720518, + "mode": "sorted-bam", + "pair_count": 5, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/contract.json new file mode 100644 index 00000000..405cc1d7 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/contract.json @@ -0,0 +1,59 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-long-20260725/source/STARlong", + "baseline_sha256": "9d990982fbda6c6a6fc6615a194e4652dca0cf1d1d582afdc7e3d2ab7cddca3e", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STARlong", + "candidate_sha256": "0f99f0668dd1fc3d8d3086ff9b7af1d2f90afe80d9dcbb0126ce9f74e7088a62", + "created_utc": "2026-07-26T00:03:35.658932Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "--seedPerReadNmax 100000" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "starlong", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": false, + "read1": "/tmp/blackstar-generalization-20260725/data/longread/SRR22582390_1.fastq", + "read1_sha256": "d8a685cb0e60574cff19aeb8ff5b860ddc7ce30c2f3a7e295441d542fa6e0dfa", + "read2": "none", + "read2_sha256": "none", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "d238aa754f877e275a70b1bb4cb038629c263e1bdb7bc479c0c5a2682b65c9b2", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-generalization-20260725/data/longread/SRR22582390_1.first-25000.fastq.gz", + "warmup_read1_sha256": "6090ffcab96458a16950b56f8adf025f88dd9cbbb337240074d0d016c1398f17", + "warmup_read2": "none", + "warmup_read2_sha256": "none" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/pairs.tsv new file mode 100644 index 00000000..331cc225 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 72.78999999999999 58.61 19.480697898062914 43188224 42242048 True +2 BA 73.01 56.67 22.380495822490076 43194368 42225664 True +3 AB 70.73 58.63 17.107309486780718 43196416 42233856 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/result.json new file mode 100644 index 00000000..0a53f822 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/starlong/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 1.7424880658322863, + "bootstrap_95_ci_percent": [ + 17.107309486780718, + 22.380495822490076 + ], + "candidate_cv_percent": 1.9421725644350616, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 72.78999999999999, + "median_candidate_wall_seconds": 58.61, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 19.480697898062914, + "median_rss_increase_percent": -2.2236973116495187, + "mode": "starlong", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/contract.json new file mode 100644 index 00000000..988a1c09 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/contract.json @@ -0,0 +1,59 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-25T23:29:32.646390Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "/tmp/blackstar-generalization-20260725/data/starsolo/3M-february-2018.txt", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "starsolo", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": false, + "read1": "/tmp/blackstar-generalization-20260725/data/starsolo/pbmc_1k_v3_R2.fastq", + "read1_sha256": "b900668353029066c47c83ddeb2dcf2d320683cff83b725ed0d86cbb39472374", + "read2": "/tmp/blackstar-generalization-20260725/data/starsolo/pbmc_1k_v3_R1.fastq", + "read2_sha256": "a70b39b38557f6cbd1aa77d51c870a20116f1e8c53aadccdaa4288bb16bb4194", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "/tmp/blackstar-generalization-20260725/data/starsolo/3M-february-2018.txt", + "starsolo_whitelist_sha256": "843a6f7038db8cb3c06f3dc21cc69d04139ffa10689780518d7a9e42dc2e819b", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "d238aa754f877e275a70b1bb4cb038629c263e1bdb7bc479c0c5a2682b65c9b2", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-generalization-20260725/data/starsolo/pbmc_1k_v3_R2.first-250000.fastq.gz", + "warmup_read1_sha256": "8c06cf9b969f731104c88ef483ec7c5bd66eee7150b9d8ae8169eb7c3f84476b", + "warmup_read2": "/tmp/blackstar-generalization-20260725/data/starsolo/pbmc_1k_v3_R1.first-250000.fastq.gz", + "warmup_read2_sha256": "64b3d0374de2744fc0b3a0a31db61a74596f725c089721dbe077eb2209a2fbb7" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/pairs.tsv new file mode 100644 index 00000000..fa5979a7 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 153.45 155.65 -1.4336917562724127 46634652 44384944 True +2 BA 153.44 147.64 3.779979144942656 46632600 44379820 True +3 AB 166.71 149.23 10.485273828804521 46638236 44381352 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/result.json new file mode 100644 index 00000000..00b2cb21 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/starsolo/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 4.851279319866566, + "bootstrap_95_ci_percent": [ + -1.4336917562724127, + 10.485273828804521 + ], + "candidate_cv_percent": 2.8114335986220533, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 153.45, + "median_candidate_wall_seconds": 149.23, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 3.779979144942656, + "median_rss_increase_percent": -4.8318147629792545, + "mode": "starsolo", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": false +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json new file mode 100644 index 00000000..2a830537 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome-thread-invariance/result.json @@ -0,0 +1,45 @@ +{ + "accepted": true, + "digests": { + "candidate-t1": { + "gene_counts_sha256": "ee1e46e35e495dc091c6c5aa8ef2dabc59811e83e8c6c961d2880bce41d2c4f5", + "genomic_bam_sha256": "d75786b32d9c360e99c8ef0e570e156b907222ffb0b73c330df362a88753aa0b", + "junctions_sha256": "0fd9bf0953f492f6802cae73e06fd3206c164016ed651a145cce9c6c21492e41", + "transcriptome_alignment_set_sha256": "5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86", + "transcriptome_primary_sha256": "f9f1417857e57087d5ffc260a7ff5ee5740c746a070b15acad842d85bb2c9091" + }, + "candidate-t96": { + "gene_counts_sha256": "ee1e46e35e495dc091c6c5aa8ef2dabc59811e83e8c6c961d2880bce41d2c4f5", + "genomic_bam_sha256": "d75786b32d9c360e99c8ef0e570e156b907222ffb0b73c330df362a88753aa0b", + "junctions_sha256": "0fd9bf0953f492f6802cae73e06fd3206c164016ed651a145cce9c6c21492e41", + "transcriptome_alignment_set_sha256": "5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86", + "transcriptome_primary_sha256": "f9f1417857e57087d5ffc260a7ff5ee5740c746a070b15acad842d85bb2c9091" + }, + "upstream-t1": { + "gene_counts_sha256": "ee1e46e35e495dc091c6c5aa8ef2dabc59811e83e8c6c961d2880bce41d2c4f5", + "genomic_bam_sha256": "d75786b32d9c360e99c8ef0e570e156b907222ffb0b73c330df362a88753aa0b", + "junctions_sha256": "0fd9bf0953f492f6802cae73e06fd3206c164016ed651a145cce9c6c21492e41", + "transcriptome_alignment_set_sha256": "5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86", + "transcriptome_primary_sha256": "903a7e8b900ad928c3e90b3c50e89f4f5ed53ac93211402ec64d38f419e03827" + }, + "upstream-t96": { + "gene_counts_sha256": "ee1e46e35e495dc091c6c5aa8ef2dabc59811e83e8c6c961d2880bce41d2c4f5", + "genomic_bam_sha256": "d75786b32d9c360e99c8ef0e570e156b907222ffb0b73c330df362a88753aa0b", + "junctions_sha256": "0fd9bf0953f492f6802cae73e06fd3206c164016ed651a145cce9c6c21492e41", + "transcriptome_alignment_set_sha256": "5672c7e1447d4e4900cef79dc063e74caac1c90e72140583d1661e5df993dd86", + "transcriptome_primary_sha256": "b6b772c7b04b1cca7b5de4b7e0eb4e3a314e3bd8e4c1d83fd0314fe15ccdbb86" + } + }, + "gates": { + "candidate_primary_flags_thread_invariant": true, + "gene_counts_identical": true, + "genomic_bams_identical": true, + "junctions_identical": true, + "timing_independent_final_metrics_identical": true, + "transcriptome_alignment_sets_identical": true + }, + "observations": { + "upstream_primary_flags_thread_invariant": false + }, + "schema": "blackstar-transcriptome-thread-invariance-v1" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/contract.json new file mode 100644 index 00000000..aff692d8 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/contract.json @@ -0,0 +1,60 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-deterministic-build-20260726/source/STAR", + "candidate_sha256": "297db5482236d970b2b19fed6016c1f9981d973fdbd6784a1700f1a7bba40635", + "created_utc": "2026-07-26T02:50:30.481754Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "transcriptome-bam", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": true, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "4f7cd49309e03a573ed68b863569a8318eb42e2268b4a3641a380ce5fcee8c2e", + "pair_driver_sha256": "451bc8fee44053a48741eeeb64e1e6c6d81e0156782c95a6647aec935550d662", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "transcriptome_primary_oracle": "upstream-equivalent records after clearing SAM flag 0x100; exact candidate primary flags across all candidate runs", + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "warmup_read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "warmup_read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "warmup_read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/pairs.tsv new file mode 100644 index 00000000..bad9ae05 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 217.32999999999998 213.96 1.5506372797128682 51776576 49792964 True +2 BA 219.09 213.32999999999998 2.629056552101885 51769704 49796676 True +3 AB 212.63 213.57999999999998 -0.44678549593189515 51796288 49804068 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json new file mode 100644 index 00000000..a84f71fb --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/transcriptome/result.json @@ -0,0 +1,45 @@ +{ + "accepted": true, + "baseline_cv_percent": 1.5436287031370335, + "bootstrap_95_ci_percent": [ + -0.44678549593189515, + 2.629056552101885 + ], + "candidate_cv_percent": 0.14849855980748802, + "candidate_transcriptome_primary_digests": [ + { + "run": "warmup-1-candidate", + "sha256": "a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc" + }, + { + "run": "pair-01-2-candidate", + "sha256": "a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc" + }, + { + "run": "pair-02-1-candidate", + "sha256": "a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc" + }, + { + "run": "pair-03-2-candidate", + "sha256": "a993252b865e62ce17a6f2bb594187e69e1e869e1f5f679ba62dd00f37ca61dc" + } + ], + "gates": { + "candidate_transcriptome_primary_determinism": true, + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 217.32999999999998, + "median_candidate_wall_seconds": 213.57999999999998, + "median_gain_at_least_2_percent": false, + "median_improvement_percent": 1.5506372797128682, + "median_rss_increase_percent": -3.8239299562798434, + "mode": "transcriptome-bam", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": false +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/contract.json b/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/contract.json new file mode 100644 index 00000000..d35985ee --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/contract.json @@ -0,0 +1,59 @@ +{ + "baseline_bin": "/tmp/blackstar-upstream-specialized-20260725/source/STAR", + "baseline_sha256": "11fc7da05151720ea6245f6b66f40a33c96e46d934f97204dfb9f2ee3e148d85", + "candidate_bin": "/tmp/blackstar-generalization-build-20260725/source/STAR", + "candidate_sha256": "fb82a5cf2fb0fdd285f9d211045d3b11ad0d5a91d360bda3874c639dc6848489", + "created_utc": "2026-07-26T00:17:44.494945Z", + "environment": { + "ALLOW_OMP_THREAD_BINDING": "unset", + "BAM_SORT_RAM": "unset", + "GENOME_LOAD_MODE": "unset", + "OMP_DYNAMIC": "unset", + "OMP_PLACES": "unset", + "OMP_PROC_BIND": "unset", + "READ_FILES_COMMAND": "unset", + "STARSOLO_WHITELIST": "unset", + "STAR_EXTRA_ARGS": "unset" + }, + "genome_core_sizes": { + "Genome": 3237267210, + "SA": 25137570409, + "SAindex": 1565873619 + }, + "genome_dir": "/tmp/blackstar-generalization-20260725/index", + "genome_parameters_sha256": "9f94d198b96294a04c990c97851da78813f5726098c8816f9bb3ab3b5532fd19", + "margin_percent": 2.0, + "max_cv_percent": 5.0, + "max_rss_increase_percent": 5.0, + "mode": "two-pass", + "orders": [ + "AB", + "BA", + "AB" + ], + "pairs": 3, + "quiet_duration": 300.0, + "quiet_gate_skipped": false, + "read1": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CXW.fastq", + "read1_sha256": "4d4f80b7cd721a5861d40bd57279683829f23fdc6ed1ad3269836b68ba952305", + "read2": "/tmp/blackstar-generalization-20260725/data/bulk76_ENCFF000CYM.fastq", + "read2_sha256": "9101c0e07e3a4ee01c495ed7fab0ecb90633d5143452c7f87d9c608d4154b8d5", + "schema": "blackstar-generalization-pairs-v1", + "seed": 20260725, + "settle_seconds": 5.0, + "starsolo_whitelist": "none", + "starsolo_whitelist_sha256": "none", + "threads": 96, + "tools": { + "comparator_sha256": "1f954fac878a21705bb6550b12bd198e35736034456d40dd75537f29b68a19cf", + "pair_driver_sha256": "d238aa754f877e275a70b1bb4cb038629c263e1bdb7bc479c0c5a2682b65c9b2", + "quiet_gate_sha256": "a9e7210f5e25e1ae079c308c9d70dc4e1df984e97f3a55328e1ccb40f6164da2", + "runner_sha256": "b7a06538b18af9787932f6043cd3b1ad22402a22b6499b3fd0cacbc09ca382de" + }, + "warmup_order": "BA", + "warmup_position": "after_quiet_gate", + "warmup_read1": "/tmp/blackstar-a00-public/ENCFF000CXW.first-250000.fastq.gz", + "warmup_read1_sha256": "ff82aa39a6364e0ef135f77bcc478bcea872afb69471af79f403e72a9179d248", + "warmup_read2": "/tmp/blackstar-a00-public/ENCFF000CYM.first-250000.fastq.gz", + "warmup_read2_sha256": "6507ffb562a348c43323b1a0b06ad882bf8f6f6bb5462b5eb8dbe5e128ba0785" +} diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/pairs.tsv b/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/pairs.tsv new file mode 100644 index 00000000..a5331bc3 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/pairs.tsv @@ -0,0 +1,4 @@ +pair order baseline_wall_seconds candidate_wall_seconds improvement_percent baseline_max_rss_kib candidate_max_rss_kib correctness_passed +1 AB 223.91 177.05 20.928051449242993 51163048 46664352 True +2 BA 223.97 178.61 20.252712416841533 51149884 46679956 True +3 AB 207.04 175.07999999999998 15.436630602782076 51160168 46678456 True diff --git a/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/result.json b/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/result.json new file mode 100644 index 00000000..44f77525 --- /dev/null +++ b/docs/benchmarks/Q02-cross-workload-20260726/results/two-pass/result.json @@ -0,0 +1,26 @@ +{ + "accepted": true, + "baseline_cv_percent": 4.469522538123011, + "bootstrap_95_ci_percent": [ + 15.436630602782076, + 20.928051449242993 + ], + "candidate_cv_percent": 0.9999042366592895, + "gates": { + "correctness": true, + "pair_count": true, + "rss": true, + "variability": true, + "wall_time_noninferiority": true + }, + "margin_percent": 2.0, + "median_baseline_wall_seconds": 223.91, + "median_candidate_wall_seconds": 177.05, + "median_gain_at_least_2_percent": true, + "median_improvement_percent": 20.252712416841533, + "median_rss_increase_percent": -8.760158879853561, + "mode": "two-pass", + "pair_count": 3, + "schema": "blackstar-generalization-result-v1", + "superiority_2_percent": true +} diff --git a/docs/experiments/Q02-cross-workload-generalization.md b/docs/experiments/Q02-cross-workload-generalization.md index 95f24ed0..90030b10 100644 --- a/docs/experiments/Q02-cross-workload-generalization.md +++ b/docs/experiments/Q02-cross-workload-generalization.md @@ -181,6 +181,18 @@ and deterministic primary flags, gene counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared-memory cross-binary lifecycle, and transformed-genome output. +## Retained Evidence Package + +The bounded [Q02 receipt package](../benchmarks/Q02-cross-workload-20260726/README.md) +is committed with the exact qualification receipt, per-mode contracts, pair +measurements, aggregate result objects, and a SHA-256 manifest. It preserves +the evidence behind both accepted and rejected claims. + +Large inputs, genome indexes, binaries, BAMs, logs, and generated output trees +remain outside Git. Their hashes are retained in the contracts. Absolute paths +inside those contracts record the original ephemeral execution environment and +are not expected to resolve in a fresh clone. + ## Negative and Limiting Evidence - The first three-pair sorted-BAM series had 6.59 percent upstream CV and was diff --git a/docs/experiments/README.md b/docs/experiments/README.md index 65bed6a5..c1ae2434 100644 --- a/docs/experiments/README.md +++ b/docs/experiments/README.md @@ -60,6 +60,8 @@ claim an ordinary-path speedup. - [Q02](Q02-cross-workload-generalization.md): complete cross-workload hardening and compatibility record; nine public series passed, while the exclusive-node single-end timing series retained a variability-gate failure. + Its bounded receipts are tracked under + [`docs/benchmarks/Q02-cross-workload-20260726/`](../benchmarks/Q02-cross-workload-20260726/README.md). ## Quiet-System Gate @@ -83,6 +85,9 @@ Large raw evidence is stored outside Git under: `benchmarks/labs///` -Small, anonymous summaries needed by public claims are copied into -`docs/architecture/evidence/`. Never place customer identifiers or absolute -internal paths in tracked public artifacts. +Bounded, anonymous contracts, pair data, aggregate results, and qualification +receipts needed to audit public claims are copied into +`docs/benchmarks//` with a checksum manifest. Claim ledgers +derived from those receipts live under `docs/architecture/evidence/`. Never +place customer identifiers, credentials, or absolute internal mount and home +paths in tracked public artifacts. From b10c14c6515b62f3e730c4e25a3b6dca20caa508 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 18:59:28 +0000 Subject: [PATCH 15/22] Document release boundaries and deferred STARsolo work --- CHANGELOG.md | 4 ++++ README.md | 14 ++++++++++++++ docs/BLACKSTAR_RELEASE.md | 4 ++++ docs/COMPATIBILITY.md | 5 +++++ docs/MIGRATING_FROM_STAR.md | 5 +++++ docs/VERSIONING.md | 13 +++++++++++++ docs/experiments/ROADMAP.md | 19 +++++++++++++++++++ 7 files changed, 64 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db131036..0b60bc64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,10 @@ remains available in `CHANGES.md` and `RELEASEnotes.md`. - Make TranscriptomeSAM primary-alignment flags deterministic across worker schedules while preserving the complete alignment set and later inherited random-stream position. +- Preserve the bounded Q02 contracts, pair measurements, aggregate decisions, + and qualification receipt in a checksummed, tracked evidence package. +- Record STARsolo post-mapping profiling as a deferred, profile-first + opportunity without making a speed claim or scheduling an implementation. These changes remain outside the current release boundary. The Q02 single-end timing series failed its variability gate, and neither a release nor an external diff --git a/README.md b/README.md index e0f7a89d..9b466814 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,19 @@ BlackSTAR is not the official STAR project and is not affiliated with or endorsed by the original STAR authors. The upstream lineage, license, and scientific citation are preserved in [ATTRIBUTION.md](ATTRIBUTION.md). +## Release and Development Status + +`v1.0.0` is the current stable BlackSTAR release. Repository source can be +ahead of that tag: changes listed under `Unreleased` in +[CHANGELOG.md](CHANGELOG.md), including the Q02 hardening candidate, are not +part of the stable release contract until they are deliberately qualified, +versioned, and tagged. + +Use release artifacts for stable adoption. When evaluating an unreleased +checkout, record the full Git commit, `STAR --version-json`, and executable +SHA-256; the compatibility-shaped `STAR --version` token alone does not +distinguish every development revision. + ## Why BlackSTAR The current qualified release adds: @@ -85,6 +98,7 @@ handling, validation, and package constraints. - [Compatibility contract](docs/COMPATIBILITY.md) - [Performance evidence and limitations](docs/PERFORMANCE.md) +- [Q02 cross-workload receipts](docs/benchmarks/Q02-cross-workload-20260726/README.md) - [Architecture Atlas](docs/architecture/README.md) - [Release boundary](docs/BLACKSTAR_RELEASE.md) - [Release acceptance](docs/BLACKSTAR_ACCEPTANCE.md) diff --git a/docs/BLACKSTAR_RELEASE.md b/docs/BLACKSTAR_RELEASE.md index 4678246e..22c96481 100644 --- a/docs/BLACKSTAR_RELEASE.md +++ b/docs/BLACKSTAR_RELEASE.md @@ -6,6 +6,10 @@ genome-format boundaries while adding genome-generation, persistent named-sequence insertion, and high-thread alignment improvements. The transitional executable lineage token is `2.7.11b-blackstar.3`. +This document describes the `v1.0.0` release boundary. Changes and measurements +explicitly labeled `Unreleased` or Q02 Labs evidence are excluded until a later +candidate passes release qualification and receives its own immutable tag. + The qualified release target is x86-64 Linux. Release automation emits a baseline x86-64 artifact and an explicitly labeled AVX2 artifact. Inherited macOS source support has not been recertified for the BlackSTAR-specific paths diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 8f5f583a..3a7eddad 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -3,6 +3,11 @@ This document defines the compatibility promises made by stable BlackSTAR releases. Anything not stated here remains best-effort. +Stable promises attach to release tags, not merely to the tip of `main` or an +unreleased source checkout. An explicitly labeled unreleased section documents +a candidate behavior and its evidence, but does not extend the stable contract +until a later release deliberately promotes it. + ## Compatibility Identities BlackSTAR tracks three separate identities: diff --git a/docs/MIGRATING_FROM_STAR.md b/docs/MIGRATING_FROM_STAR.md index f874b2d4..f59b82ff 100644 --- a/docs/MIGRATING_FROM_STAR.md +++ b/docs/MIGRATING_FROM_STAR.md @@ -16,6 +16,11 @@ that evaluation can begin without redesigning an RNA-seq workflow. 6. Retain official STAR as an explicit fallback until downstream validation is complete. +Prefer an immutable BlackSTAR release tag for adoption. If the evaluation +requires an unreleased candidate, pin its full commit and executable checksum, +treat its `Unreleased` changelog entry as outside the stable support boundary, +and repeat the release gates relevant to the intended workflow. + Do not compare runs that use different reference files, annotations, output modes, decompression commands, thread counts, storage tiers, or concurrent system load. diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index b67f2999..840a11eb 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -17,6 +17,19 @@ These values must not be collapsed into one string. A project release can change without changing the genome format, and a compatibility-base update can require a new BlackSTAR major or minor release. +## Development Checkouts + +An untagged checkout is not a release, even when its legacy `STAR --version` +token matches the most recent release. Development revisions must be identified +by full Git commit, `STAR --version-json`, executable SHA-256, CPU target, and +build provenance. `CHANGELOG.md` is authoritative for whether a source change +remains unreleased. + +Performance and compatibility evidence from a development candidate applies to +the exact recorded binary. It becomes a stable project promise only after the +candidate is rebuilt from the protected release commit, passes release +qualification, and receives an immutable semantic-version tag. + ## Semantic Versioning BlackSTAR release tags use `vMAJOR.MINOR.PATCH`. diff --git a/docs/experiments/ROADMAP.md b/docs/experiments/ROADMAP.md index b9bdc4f7..90d6aed5 100644 --- a/docs/experiments/ROADMAP.md +++ b/docs/experiments/ROADMAP.md @@ -22,12 +22,29 @@ until it passes cumulative qualification and is deliberately promoted. | A09 | Toolchain | LTO and profile-guided optimization improve the accepted cumulative alignment stack without semantic changes. | Rejected; LTO and PGO each gained about 1.2%, below the 2% practical gate | A06 | | Q01 | Cumulative alignment qualification | The complete H01+A02+A05+A06 stack preserves release behavior and generalizes across private, shared, compressed, BAM, affinity, sanitizer, compatibility, and package gates. | Complete; promoted in blackstar.2 | A06 and A09 decision | | Q02 | Cross-workload generalization | The released high-thread stack and new compatibility hardening preserve behavior beyond paired gene-count-only RNA-seq. | Complete; nine public series passed, single-end timing remains unresolved, no release promotion | Q01 and successor transition | +| S01 | STARsolo post-mapping parallelism | Phase-resolved profiling may identify deterministic parallelism in barcode aggregation, Solo-record ingestion, per-cell UMI collapse, cell filtering, and matrix output. | Deferred future opportunity; no implementation scheduled | Q02 and dedicated STARsolo profile | | I01 | Genome preparation | Parallel reverse-complement and bounded private prefix histograms reduce serial setup. | Proposed | Cumulative alignment qualification | | I02 | SA packing | Record-block partitioning permits deterministic disjoint-byte parallel packing. | Proposed | I01 | | I03 | Junction merge | Partitioned merge and rank calculation reduce the remaining serial junction stage. | Proposed | I02 | | I04 | Suffix sorting | Comparator correction plus inlined multikey radix sorting reduces dominant bin-sort work. | Proposed | I03 | | I05 | Index v2 | Pinned libsais64 may justify an opt-in incompatible format only if I04 is insufficient. | Conditional | I04 decision | +## Deferred STARsolo Opportunity + +Q02 established STARsolo compatibility and noninferiority, not a speed claim: +the 10x v3 Gene fixture improved by a median 3.78 percent, with a 95 percent +interval from -1.43 to 10.49 percent. BlackSTAR's shared alignment changes are +active in STARsolo, but they did not target its distinct post-mapping barcode, +UMI, filtering, and matrix-generation paths. + +If S01 is prioritized later, begin with phase-resolved profiling on a +realistically sized dataset. Do not assume that the currently visible serial +loops dominate wall time. Any implementation must preserve deterministic +output and qualify at least 10x 3-prime Gene, single-nucleus GeneFull, UMI +deduplication, cell filtering, and complete matrix output before broader +claims. Additional 10x chemistries, multimapper modes, and Velocyto output +belong in the cumulative gate. + ## Stop Conditions - Reject a change that fails any correctness oracle. @@ -50,5 +67,7 @@ until it passes cumulative qualification and is deliberately promoted. - Q02 TranscriptomeSAM output intentionally stabilizes the primary transcript flag across worker schedules. The complete upstream alignment set remains the compatibility oracle after clearing only flag `0x100`. +- Do not begin S01 without an explicit prioritization decision. Its first + artifact is a phase profile, not a source change. - I05 begins only after a documented I04 decision and retains the default v1 index format. From cba8e2dbf1813bbe0c5a9889f6d2f2137e632ddb Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:20:06 +0000 Subject: [PATCH 16/22] Prepare BlackSTAR 1.1.0 release --- .github/repository-settings.json | 8 +- .github/workflows/blackstar-ci.yml | 55 ++- .github/workflows/release.yml | 20 +- CHANGELOG.md | 11 +- README.md | 7 +- docs/BLACKSTAR_ACCEPTANCE.md | 324 ++++++++---------- docs/BLACKSTAR_PROMOTION.md | 14 +- docs/BLACKSTAR_RELEASE.md | 16 +- docs/COMPATIBILITY.md | 12 +- docs/PERFORMANCE.md | 14 +- docs/VERSIONING.md | 2 +- docs/architecture/README.md | 1 + docs/architecture/STATUS_HISTORY.md | 4 +- docs/architecture/claims.tsv | 6 + .../pdf/F25-version-compatibility.pdf | Bin 26243 -> 26242 bytes .../pdf/F27-cross-workload-generalization.pdf | Bin 30636 -> 30490 bytes .../pdf/F29-v1.1-release-qualification.pdf | Bin 0 -> 33830 bytes .../src/F25-version-compatibility.mmd | 2 +- .../src/F27-cross-workload-generalization.mmd | 4 +- .../src/F29-v1.1-release-qualification.mmd | 32 ++ .../svg/F25-version-compatibility.svg | 4 +- .../svg/F27-cross-workload-generalization.svg | 4 +- .../svg/F29-v1.1-release-qualification.svg | 2 + docs/architecture/figures.json | 26 +- .../RCQ-1.1.0-source-20260726/README.md | 46 +++ .../RCQ-1.1.0-source-20260726/SHA256SUMS | 2 + .../qualification-receipt.tsv | 34 ++ .../Q02-cross-workload-generalization.md | 16 +- docs/experiments/README.md | 10 +- docs/experiments/ROADMAP.md | 15 +- docs/releases/1.1.0-release-notes.md | 52 +++ .../2.7.11b-blackstar.2-acceptance.md | 189 ++++++++++ .../validate_successor_metadata.py | 35 +- extras/scripts/buildBlackSTARRelease.sh | 20 +- extras/scripts/compareBlackSTARReleases.sh | 5 + source/VERSION | 2 +- 36 files changed, 727 insertions(+), 267 deletions(-) create mode 100644 docs/architecture/diagrams/pdf/F29-v1.1-release-qualification.pdf create mode 100644 docs/architecture/diagrams/src/F29-v1.1-release-qualification.mmd create mode 100644 docs/architecture/diagrams/svg/F29-v1.1-release-qualification.svg create mode 100644 docs/benchmarks/RCQ-1.1.0-source-20260726/README.md create mode 100644 docs/benchmarks/RCQ-1.1.0-source-20260726/SHA256SUMS create mode 100644 docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv create mode 100644 docs/releases/1.1.0-release-notes.md create mode 100644 docs/releases/2.7.11b-blackstar.2-acceptance.md diff --git a/.github/repository-settings.json b/.github/repository-settings.json index 32cdc10d..d2266a66 100644 --- a/.github/repository-settings.json +++ b/.github/repository-settings.json @@ -41,7 +41,13 @@ }, "required_status_checks": { "contexts": [ - "build-and-test" + "build-and-test", + "compiler-gcc", + "compiler-clang", + "starlong-build-and-smoke", + "release-portability", + "codeql-c-cpp", + "codeql-python" ], "strict": true }, diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index 0de8bdc0..75df87e7 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -90,31 +90,49 @@ jobs: - name: Verify release identity and OpenMP linkage run: | + version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" + baseline="dist/blackstar-${version}-linux-x86_64-baseline" + avx2="dist/blackstar-${version}-linux-x86_64-avx2" test "$(source/STAR --version)" = "2.7.11b-blackstar.3" EXPECTED_CPU_TARGET=avx2 extras/tests/scripts/testBlackstarVersion.sh ldd source/STAR | grep -Eq 'libgomp|libomp' - test -f dist/blackstar-1.0.0-linux-x86_64-baseline.spdx.json - test -f dist/blackstar-1.0.0-linux-x86_64-baseline/LICENSE - test -f dist/blackstar-1.0.0-linux-x86_64-baseline/ATTRIBUTION.md - test -f dist/blackstar-1.0.0-linux-x86_64-avx2.spdx.json + test -f "${baseline}.spdx.json" + test -x "${baseline}.STAR" + test -f "${baseline}.STAR.sha256" + test -f "${baseline}.build-info.tsv" + test -f "${baseline}.compatibility.tsv" + test -f "${baseline}.ldd.txt" + test -f "${baseline}/LICENSE" + test -f "${baseline}/ATTRIBUTION.md" + test -f "${avx2}.spdx.json" + test -x "${avx2}.STAR" + test -f "${avx2}.STAR.sha256" + test -f "${avx2}.build-info.tsv" + test -f "${avx2}.compatibility.tsv" + test -f "${avx2}.ldd.txt" + ( + cd dist + sha256sum -c "$(basename "${baseline}").STAR.sha256" + sha256sum -c "$(basename "${avx2}").STAR.sha256" + ) grep -Fxq $'cpu_target\tbaseline' \ - dist/blackstar-1.0.0-linux-x86_64-baseline/compatibility.tsv + "${baseline}/compatibility.tsv" grep -Fxq $'ymm_instructions\tabsent' \ - dist/blackstar-1.0.0-linux-x86_64-baseline/compatibility.tsv + "${baseline}/compatibility.tsv" grep -Fxq $'cpu_target\tavx2' \ - dist/blackstar-1.0.0-linux-x86_64-avx2/compatibility.tsv + "${avx2}/compatibility.tsv" grep -Fxq $'ymm_instructions\tpresent' \ - dist/blackstar-1.0.0-linux-x86_64-avx2/compatibility.tsv - jq -e ' + "${avx2}/compatibility.tsv" + jq -e --arg version "${version}" ' .spdxVersion == "SPDX-2.3" and - (.packages[] | select(.name == "BlackSTAR").versionInfo) == "1.0.0" - ' dist/blackstar-1.0.0-linux-x86_64-baseline.spdx.json + (.packages[] | select(.name == "BlackSTAR").versionInfo) == $version + ' "${baseline}.spdx.json" test "$( jq -r .documentNamespace \ - dist/blackstar-1.0.0-linux-x86_64-baseline.spdx.json + "${baseline}.spdx.json" )" != "$( jq -r .documentNamespace \ - dist/blackstar-1.0.0-linux-x86_64-avx2.spdx.json + "${avx2}.spdx.json" )" - name: Verify release variant result equivalence @@ -210,8 +228,10 @@ jobs: CXX: ${{ matrix.cxx }} run: | make -C source -j2 STAR CXX="${CXX}" + version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" test "$(source/STAR --version)" = "2.7.11b-blackstar.3" - source/STAR --version-json | jq -e '.blackstar_version == "1.0.0"' + source/STAR --version-json | + jq -e --arg version "${version}" '.blackstar_version == $version' ldd source/STAR | grep -Eq 'libgomp|libomp' starlong-build-and-smoke: @@ -285,15 +305,16 @@ jobs: env: DIST_DIR: ${{ runner.temp }}/portable-dist run: | + version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" for target in baseline avx2; do - compatibility="${DIST_DIR}/blackstar-1.0.0-linux-x86_64-${target}/compatibility.tsv" + compatibility="${DIST_DIR}/blackstar-${version}-linux-x86_64-${target}/compatibility.tsv" minimum_glibc="$(awk -F '\t' '$1=="minimum_glibc" {print $2}' "${compatibility}")" dpkg --compare-versions "${minimum_glibc}" le "2.31" done grep -Fxq $'ymm_instructions\tabsent' \ - "${DIST_DIR}/blackstar-1.0.0-linux-x86_64-baseline/compatibility.tsv" + "${DIST_DIR}/blackstar-${version}-linux-x86_64-baseline/compatibility.tsv" grep -Fxq $'ymm_instructions\tpresent' \ - "${DIST_DIR}/blackstar-1.0.0-linux-x86_64-avx2/compatibility.tsv" + "${DIST_DIR}/blackstar-${version}-linux-x86_64-avx2/compatibility.tsv" - name: Verify release variant result equivalence env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bc55f9d..aaaf814c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -125,7 +125,7 @@ jobs: subject-path: | ${{ runner.temp }}/build-1/*.tar.gz ${{ runner.temp }}/build-1/*.spdx.json - ${{ runner.temp }}/build-1/*/STAR + ${{ runner.temp }}/build-1/*.STAR - name: Publish immutable release env: @@ -138,19 +138,21 @@ jobs: --verify-tag \ --title "BlackSTAR ${version}" \ --notes-file "docs/releases/${version}-release-notes.md" \ - "${RUNNER_TEMP}/build-1/${baseline}/STAR#BlackSTAR baseline Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${baseline}.STAR#BlackSTAR baseline Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${baseline}.STAR.sha256#Baseline executable SHA-256 sidecar" \ "${RUNNER_TEMP}/build-1/${baseline}.tar.gz#BlackSTAR baseline Linux x86-64 archive" \ "${RUNNER_TEMP}/build-1/${baseline}.tar.gz.sha256#Baseline archive SHA-256 sidecar" \ "${RUNNER_TEMP}/build-1/${baseline}.spdx.json#Baseline SPDX 2.3 SBOM" \ - "${RUNNER_TEMP}/build-1/${baseline}/build-info.tsv#Baseline reproducible build metadata" \ - "${RUNNER_TEMP}/build-1/${baseline}/compatibility.tsv#Baseline compatibility metadata" \ - "${RUNNER_TEMP}/build-1/${baseline}/ldd.txt#Baseline runtime linkage metadata" \ - "${RUNNER_TEMP}/build-1/${avx2}/STAR#BlackSTAR AVX2 Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${baseline}.build-info.tsv#Baseline reproducible build metadata" \ + "${RUNNER_TEMP}/build-1/${baseline}.compatibility.tsv#Baseline compatibility metadata" \ + "${RUNNER_TEMP}/build-1/${baseline}.ldd.txt#Baseline runtime linkage metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}.STAR#BlackSTAR AVX2 Linux x86-64 executable" \ + "${RUNNER_TEMP}/build-1/${avx2}.STAR.sha256#AVX2 executable SHA-256 sidecar" \ "${RUNNER_TEMP}/build-1/${avx2}.tar.gz#BlackSTAR AVX2 Linux x86-64 archive" \ "${RUNNER_TEMP}/build-1/${avx2}.tar.gz.sha256#AVX2 archive SHA-256 sidecar" \ "${RUNNER_TEMP}/build-1/${avx2}.spdx.json#AVX2 SPDX 2.3 SBOM" \ - "${RUNNER_TEMP}/build-1/${avx2}/build-info.tsv#AVX2 reproducible build metadata" \ - "${RUNNER_TEMP}/build-1/${avx2}/compatibility.tsv#AVX2 compatibility metadata" \ - "${RUNNER_TEMP}/build-1/${avx2}/ldd.txt#AVX2 runtime linkage metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}.build-info.tsv#AVX2 reproducible build metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}.compatibility.tsv#AVX2 compatibility metadata" \ + "${RUNNER_TEMP}/build-1/${avx2}.ldd.txt#AVX2 runtime linkage metadata" \ "LICENSE#MIT license" \ "ATTRIBUTION.md#Upstream lineage and citation" diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b60bc64..a695d8b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ remains available in `CHANGES.md` and `RELEASEnotes.md`. ## Unreleased +_No changes yet._ + +## 1.1.0 - 2026-07-26 + - Harden SAM-input chunk sizing and genome-insert annotation and identity validation. - Bound automatic index strategies by cgroup-aware available memory. @@ -22,9 +26,10 @@ remains available in `CHANGES.md` and `RELEASEnotes.md`. - Record STARsolo post-mapping profiling as a deferred, profile-first opportunity without making a speed claim or scheduling an implementation. -These changes remain outside the current release boundary. The Q02 single-end -timing series failed its variability gate, and neither a release nor an external -deployment is authorized by this entry. +The Q02 single-end timing series failed its variability gate and does not +support a speed claim. STARsolo and TranscriptomeSAM performance remain +noninferior-only results. Release publication does not authorize integration +into an external pipeline. ## 1.0.0 - 2026-07-24 diff --git a/README.md b/README.md index 9b466814..a6ef508a 100644 --- a/README.md +++ b/README.md @@ -11,11 +11,10 @@ scientific citation are preserved in [ATTRIBUTION.md](ATTRIBUTION.md). ## Release and Development Status -`v1.0.0` is the current stable BlackSTAR release. Repository source can be +`v1.1.0` is the current stable BlackSTAR release. Repository source can be ahead of that tag: changes listed under `Unreleased` in -[CHANGELOG.md](CHANGELOG.md), including the Q02 hardening candidate, are not -part of the stable release contract until they are deliberately qualified, -versioned, and tagged. +[CHANGELOG.md](CHANGELOG.md) are not part of the stable release contract until +they are deliberately qualified, versioned, and tagged. Use release artifacts for stable adoption. When evaluating an unreleased checkout, record the full Git commit, `STAR --version-json`, and executable diff --git a/docs/BLACKSTAR_ACCEPTANCE.md b/docs/BLACKSTAR_ACCEPTANCE.md index 0683f23b..f70e47a9 100644 --- a/docs/BLACKSTAR_ACCEPTANCE.md +++ b/docs/BLACKSTAR_ACCEPTANCE.md @@ -1,189 +1,169 @@ -# BlackSTAR 2.7.11b-blackstar.2 Acceptance Record +# BlackSTAR 1.1.0 Acceptance Record ## Verdict -BlackSTAR `2.7.11b-blackstar.2` has passed the technical gates for an x86-64 -Linux release. It inherits the accepted index-generation, genome-insert, -integrity, and deployment boundary from `2.7.11b-blackstar.1` and adds the -cumulatively qualified H01, A02, A05, and A06 alignment changes. +BlackSTAR 1.1.0 is accepted for an x86-64 Linux release after the protected +release commit passes every required GitHub check and the tagged release +workflow reproduces, compares, attests, and publishes both CPU variants. -The cumulative qualification record is commit -`9998c445c5b87adacd2a4663bd964ce744aea300`. Paired timing used cumulative -source revision `7b31a5fe5cb966c9146b99d5ca1ad81ea9c81cdb`; the final runtime -implementation commit is `6032393155317b17fef1750672f1da6770ae6042`. -The benchmark harness revision is -`9d37e0f8009c28fd72e84c3c42fa272ed53b1f71`. +This release promotes the Q02 compatibility hardening while retaining every +negative and limiting result. It does not claim a single-end speedup, a +STARsolo speedup, or a TranscriptomeSAM speedup. -This is a source and release-artifact qualification. It is not authorization -for integration into any external pipeline or production environment. +This is source and release-artifact qualification. It is not authorization for +integration into an external pipeline or production environment. -## Release Ancestry +## Qualification Anchors | Boundary | Commit or tag | Role | | --- | --- | --- | -| Upstream STAR | `2.7.11b` at `b1edc1208d91a53bf40ebae8669f71d50b994851` | Compatibility oracle and inherited core | -| BlackSTAR prior release | `2.7.11b-blackstar.1` at `821457378fa38bfb23b061b8f11ee0a09431dda7` | Qualified index, insertion, integrity, and deployment boundary | -| Cumulative alignment qualification | `9998c445c5b87adacd2a4663bd964ce744aea300` | Qualified H01+A02+A05+A06 source and evidence | -| BlackSTAR current release | `2.7.11b-blackstar.2` | Prior release plus the qualified cumulative alignment stack | - -The complete `blackstar.1` acceptance record is preserved at -[releases/2.7.11b-blackstar.1-acceptance.md](releases/2.7.11b-blackstar.1-acceptance.md). - -## Supported Boundary - -The supported target is x86-64 Linux and includes: - -- deterministic, memory-adaptive `genomeGenerate` suffix-array, SAindex, and - junction-index construction; -- `genomeInsert Full`, packaged `Overlay`, and cached `Delta` modes with - insert-only GTF support; -- virtual-SA no-junction Delta alignment; -- strict package identity, validation, and atomic publication; -- the upstream correctness fixes carried by BlackSTAR; -- recovery from inherited OpenMP affinity narrowing before alignment pthread - creation; -- adaptive record-safe alignment input chunks at 64 or more threads; -- NUMA-aware placement for eligible high-thread private genome loads; and -- transcript-recursion copy elision on nonmutating branches. - -The release excludes `alignReadsMulti`, persistent prefork workers, threaded -BAM-compression prototypes, A01 touched-bin reset, A02b producer/consumer -queue, and A09 LTO/PGO variants. Their source is not stacked into the release. - -## Closed Alignment Findings - -| Finding | Resolution | Acceptance evidence | -| --- | --- | --- | -| Explicit OpenMP binding could confine all STAR alignment pthreads to one physical core | Restore the complete allowed OpenMP-place union before pthread creation; avoid OpenMP initialization when recovery is not needed | Matched failure control, ordinary-path noninferiority, focused sanitizer test, and Q01 repeat | -| Coarse high-thread input chunks produced a long completion tail | Select 1 MB record-safe chunks automatically at 64 or more mapping threads while retaining legacy behavior below the threshold | Replicated uncompressed and compressed gains, lower RSS, exact outputs, and canonical BAM | -| Private genome pages incurred avoidable NUMA migration and fault work | Interleave eligible high-thread private loads while preserving inherited policy, low-thread fallback, and shared-memory behavior | Replicated 96- and 64-thread gates, policy matrix, shared-index fallback, portability test, and Q01 | -| Recursive transcript search copied unchanged state on exclude branches | Pass the current transcript by const reference and copy only mutating or terminal branches | Copy-constructor CPU reduction, five-pair end-to-end gain, compressed input, canonical BAM, and sanitizers | -| Individually accepted changes lacked one cumulative release-style gate | Compare the full stack with the qualified prior release across input, memory, output, affinity, compatibility, and package paths | Q01 cumulative qualification | - -## Cumulative Performance - -The primary public workload used GRCh38 with Ensembl 114 annotations, -12,768,316 paired 76-base ENCODE reads, local SSD, 96 requested logical CPUs, -gene counts, and three seeded order-balanced pairs per input mode. - -| Input | `blackstar.1` control | `blackstar.2` source | Median paired improvement | 95% interval | Correctness | -| --- | ---: | ---: | ---: | ---: | ---: | -| Uncompressed | 84.28 s | 56.17 s | **33.2025%** | 33.0802 to 35.8089% | 3/3 | -| `zcat` | 90.61 s | 64.03 s | **28.5399%** | 28.4661 to 29.6105% | 3/3 | - -Both arms remained below the 3% variability gate. Median peak RSS fell by -6.81% uncompressed and 6.77% through `zcat`. Every pair matched -timing-independent final metrics, splice junctions, and gene counts. - -These values are cumulative measurements against `blackstar.1`; individual -experiment percentages must not be added to them. They apply to the measured -host, corpus, index, storage, and thread count. - -## Affinity Failure Recovery - -A one-pair positive control deliberately set: - -```text -OMP_PROC_BIND=close -OMP_PLACES=cores -``` - -with 96 requested STAR threads and 2,000,000 public read pairs. -`blackstar.1` took 226.13 seconds at 186% mean CPU. The cumulative source -restored 128 OpenMP places and 256 allowed CPUs, then completed in 37.69 -seconds at 952% mean CPU. All three timing-independent comparisons passed. - -The 83.33% reduction is a matched failure-mode result, not an ordinary-path -performance estimate. H01's separate five-pair unbound gate was noninferior. - -## Shared Index and BAM Safety - -The cumulative source preloaded a 29,940,711,542-byte shared genome segment, -retained the default shared-memory NUMA policy, mapped 2,000,000 public paired -reads with `LoadAndKeep`, emitted an unsorted BAM, and passed all five checks: - -- timing-independent final metrics; -- splice junctions; -- gene counts; -- BAM presence; and -- canonical BAM records. - -The control and candidate canonical BAM digest was -`e3f67bccb149277f6f9673e204071b80afab5021c8182028ec17678cdbc750ac`. -`LoadAndRemove` then removed the test segment. This one-pair check establishes -safety, not shared-index performance. - -## Inherited Index and Insertion Qualification - -The `blackstar.1` qualification remains applicable because H01, A02, A05, and -A06 do not change index formats or genome-insert package formats. - -Inherited gates include: - -- three order-balanced full-CHM13 index-build pairs with 49.48% less mean wall - time than upstream STAR and 42/42 substantive file comparisons byte-exact; -- 24/24 Full, Overlay, and Delta hardening checks; -- 4/4 serial, bounded-parallel, and RAM-constrained SAindex strategy checks; -- named-sequence FASTA and GTF insertion, namespace collision rejection, - package relocation, stale-base rejection, corruption rejection, and atomic - cleanup; and -- full-index alignment compatibility under official upstream STAR 2.7.11b. - -## Automated and Reproducibility Gates - -The release-candidate source passed: - -- architecture provenance and public-hygiene validation; -- deterministic rendering of all canonical SVG figures; -- benchmark-harness regression tests; -- eight focused ASan/UBSan scripts; -- the deployment-selector matrix, including expected negative cases; -- 24 genome-insert hardening checks; -- four SAindex strategy checks; and -- clean reproducible release-package builds with OpenMP linkage. - -The final deployable binary and archive are built from the clean protected -`master` commit. Their exact checksums belong in `build-info.tsv`, the checksum -sidecar, and GitHub release metadata. Embedded Git provenance means a -documentation-only promotion commit changes the executable checksum, so the -final artifact checksum is not edited back into this source record. - -## Rejected Toolchain Variants - -LTO and PGO each preserved exact measured outputs and reduced binary size. -Their independent five-pair median improvements were 1.21% and 1.20%, -respectively, below the predeclared 2% practical gate. Neither variant is -included in `blackstar.2`. +| Official STAR | `2.7.11b` at `b1edc1208d91a53bf40ebae8669f71d50b994851` | Compatibility oracle and inherited core | +| Prior stable BlackSTAR | `v1.0.0` at `cd3adb609539840bcbcdccbcb2635c4edec03ac2` | Independent-project and rollback boundary | +| Final Q02 runtime source | `ef2a2560013293a3cd93403d876f50a3d5ec759c` | Cross-workload implementation and public evidence | +| Exact local release candidate | `b10c14c6515b62f3e730c4e25a3b6dca20caa508` | Dedicated-node 29-gate cumulative qualification | +| Stable release | `v1.1.0` | Protected release commit and immutable artifact identity | + +The code and test-harness tree at the exact local candidate was identical to +the Q02 runtime source; intervening changes were documentation. Release +preparation after that candidate is limited to version identity, release +records, generated architecture status, dynamic CI package paths, and required +check policy. The protected branch and tag remain authoritative for the final +commit and executable checksums. + +## Supported Additions + +BlackSTAR 1.1.0 adds the following to the 1.0.0 contract: + +- record-safe SAM-input handling when automatic high-thread chunk sizing is + active; +- cgroup-aware memory limits for automatic index strategies; +- restoration of inherited NUMA policy after eligible private genome loading; +- stricter named-sequence annotation, namespace, package-identity, relocation, + and corruption checks; +- isolated short-read and STARlong build state; +- explicit baseline x86-64 and AVX2 release variants with ISA inspection and + output-equivalence checks; and +- deterministic `TranscriptomeSAM` primary selection from the run seed and + stable read ordinal while preserving the complete transcript alignment set + and later inherited random-stream position. + +The conventional genome format remains `2.7.4a`. Full indexes retain official +STAR 2.7.11b compatibility. Overlay and Delta remain BlackSTAR-specific and +require `--genomeLoad NoSharedMemory`. + +## Exact-Candidate Qualification + +The exact candidate ran on dedicated Slurm node CA2 from two independent clean +source paths. Twenty-nine recorded gates passed: + +- baseline and AVX2 release products were byte-identical across independent + absolute source paths; +- release identity, OpenMP linkage, CPU-target metadata, ISA labels, and + baseline-versus-AVX2 result equivalence passed; +- 12/12 specialized official-STAR differential modes passed; +- 11/11 focused ASan and UBSan scripts passed; +- all 32 GenomeInsert hardening subchecks passed, including stock-STAR + full-index compatibility, cross-thread idempotence, package relocation, + corruption rejection, namespace validation, and insert-only GTF contracts; +- serial, bounded-parallel, and constrained-memory SAindex strategies produced + identical indexes; +- STARlong built independently and passed the high-thread and official-STARlong + smoke oracle; +- 12,768,316 paired 76-base public reads matched official STAR in every + timing-independent metric, splice junction, and gene count; +- the real GRCh38 GFP/GST Delta package matched the previously accepted package + byte-for-byte and matched its conventional full-index mapping oracle, + including exactly 100 GFP and 100 GST fragments; +- a full CHM13+ERCC build matched all 14 retained substantive index artifacts + byte-for-byte; and +- all 28 pre-release architecture figures reproduced exactly. + +The full-index and public-read timings from this cumulative run were +descriptive single runs, not replacement performance claims. + +## Cross-Workload Evidence + +Q02 evaluated ten public workload series and a specialized synthetic matrix. +All 36/36 public pair-level output comparisons and 12/12 specialized checks +passed their applicable metrics, junction, count, SAM, BAM, chimeric, +STARsolo, shared-memory, or transformed-genome oracle. + +| Workload | Median paired result | Release interpretation | +| --- | ---: | --- | +| Paired 150-base fragmented | 12.67% less wall time | Replicated gain | +| BySJout | 31.45% less wall time | Replicated gain | +| Chimeric detection | 31.76% less wall time | Replicated gain | +| STARlong direct RNA | 19.48% less wall time | Replicated gain under symmetric seed-limit override | +| TranscriptomeSAM | 1.55% point estimate | Noninferior; no speed claim | +| STARsolo 10x v3 Gene | 3.78% point estimate | Noninferior; no speed claim | +| Single-end 150-base | 1.67% point estimate | Correctness passed; variability gate failed; no speed claim | + +Paired 76-base, two-pass, and coordinate-sorted BAM also had positive paired +intervals, but their control-arm variability exceeded the stricter 3 percent +preference for a new release speed claim. Their compatibility evidence remains +accepted. + +## Compatibility-Visible Correction + +Official STAR chooses a `TranscriptomeSAM` primary alignment from worker-local +random state. A controlled 1-versus-96-thread run therefore produced different +primary flags for the same input. + +BlackSTAR 1.1.0 selects from `runRNGseed` and the stable input-read ordinal. The +controlled primary digest was exact across 1 and 96 threads. The complete +transcript alignment set after clearing only flag `0x100`, genomic BAM records, +gene counts, junctions, and timing-independent metrics remained exact. One +legacy random draw is retained so later inherited random choices do not shift. + +This is an intentional, documented compatibility correction rather than a +claim of byte identity with one particular official-STAR primary flag. + +## Required Protected Checks + +The tracked and live branch policy requires all of: + +1. `build-and-test`; +2. `compiler-gcc`; +3. `compiler-clang`; +4. `starlong-build-and-smoke`; +5. `release-portability`; +6. `codeql-c-cpp`; and +7. `codeql-python`. + +The portability job builds baseline and AVX2 packages on Ubuntu 20.04 with +glibc 2.31, verifies the ABI and ISA floors, and compares variant outputs. The +release workflow repeats two clean builds from different absolute paths before +publication. ## Residual Risk -- Performance qualification covers one x86-64 Linux host, one GCC version, - one public paired short-read corpus, one index, one high-thread count, and - local SSD. -- Long reads, single-end performance, network-storage performance, BAM sorting, - macOS, and non-x86-64 targets were not performance-qualified. -- NUMA auto-placement activates only for eligible high-thread private loads; - callers can select `--genomeLoadNumaPolicy Default` to retain inherited - placement. -- Overlay and Delta still require `NoSharedMemory`. +- Release qualification covers x86-64 Linux. Inherited macOS and non-x86 + source paths are not release-qualified. +- Performance evidence covers the recorded CPU family, toolchains, fixtures, + local storage, and primarily 96-thread runs. +- Full-index acceleration can use substantially more memory than official + STAR; host-memory assessment remains mandatory. +- Single-end performance is unresolved and must not be described as faster. +- STARsolo post-mapping work is a deferred profile-first opportunity. +- Both official STARlong and BlackSTAR STARlong fail the real direct-RNA + fixture under the inherited default `seedPerReadNmax`; the accepted + comparison raises the limit equally in both arms. +- Overlay and Delta packages are not loadable by official STAR. - Bundled HTSlib remains old and is not an accepted base for new compression work. -- Broad process-lifetime allocations and longstanding inherited compiler - warnings remain outside this release. - -These limitations constrain claims and deployment scope; they are not observed -correctness regressions in the supported release boundary. ## Publication Requirements -1. Push the release-candidate branch and require the protected - `build-and-test` check. -2. Preserve linear history when promoting the tested commit to `master`. -3. Create annotated tag `2.7.11b-blackstar.2` at the exact tested `master` - commit. -4. Build and publish the exact clean-commit binary, deterministic archive, - checksum sidecar, `build-info.tsv`, and `ldd.txt`. -5. Verify the live default branch, tag target, required check, release assets, - and asset digests after publication. - -Publication does not authorize deployment into an external pipeline. +1. Merge through a pull request with every required check successful. +2. Verify the protected `main` push reruns the same required checks. +3. Create annotated tag `v1.1.0` at that exact protected commit. +4. Run the release workflow from the tag. +5. Require byte-identical independent package builds for baseline and AVX2. +6. Publish the binaries, deterministic archives, SHA-256 sidecars, + compatibility metadata, build metadata, linkage metadata, SPDX SBOMs, + license, attribution, and provenance attestations. +7. Verify public asset digests, executable identities, installation smoke, + prior-release rollback, and repository recovery evidence. + +The historical blackstar.2 record is retained at +[2.7.11b-blackstar.2-acceptance.md](releases/2.7.11b-blackstar.2-acceptance.md). diff --git a/docs/BLACKSTAR_PROMOTION.md b/docs/BLACKSTAR_PROMOTION.md index 36b8b8e6..b548edbd 100644 --- a/docs/BLACKSTAR_PROMOTION.md +++ b/docs/BLACKSTAR_PROMOTION.md @@ -7,11 +7,11 @@ authorize deployment and does not replace environment-specific pipeline tests. | Field | Value | |---|---| -| Version | `2.7.11b-blackstar.2` | -| Prior qualified release | `2.7.11b-blackstar.1` at `821457378fa38bfb23b061b8f11ee0a09431dda7` | -| Cumulative qualification record | `9998c445c5b87adacd2a4663bd964ce744aea300` | -| Alignment benchmark source | `7b31a5fe5cb966c9146b99d5ca1ad81ea9c81cdb` | -| Alignment benchmark executable SHA-256 | `6b08a7925022c6a03813660c69d0b0bd1ed98a377ac9c1136dd987e3d4c8f85e` | +| Version | `1.1.0` | +| Prior qualified release | `v1.0.0` at `cd3adb609539840bcbcdccbcb2635c4edec03ac2` | +| Exact source qualification | `b10c14c6515b62f3e730c4e25a3b6dca20caa508` | +| Cross-workload runtime source | `ef2a2560013293a3cd93403d876f50a3d5ec759c` | +| Deployable executable SHA-256 | Read from the selected `v1.1.0` release asset `build-info.tsv` | | Platform | x86-64 Linux | | Required linkage | `libgomp` or `libomp` | @@ -43,7 +43,7 @@ commit. extras/scripts/selectBlackSTAR.sh \ --candidate /opt/blackstar/STAR \ --candidate-sha256 CANDIDATE_SHA256 \ - --candidate-version 2.7.11b-blackstar.2 \ + --candidate-version 2.7.11b-blackstar.3 \ --fallback /opt/star-stock/STAR \ --fallback-sha256 STOCK_SHA256 \ --fallback-version 2.7.11b \ @@ -72,7 +72,7 @@ Do not bypass the selector with an unpinned binary path. extras/scripts/selectBlackSTAR.sh \ --candidate /opt/blackstar/STAR \ --candidate-sha256 CANDIDATE_SHA256 \ - --candidate-version 2.7.11b-blackstar.2 \ + --candidate-version 2.7.11b-blackstar.3 \ --fallback /opt/star-stock/STAR \ --fallback-sha256 STOCK_SHA256 \ --fallback-version 2.7.11b \ diff --git a/docs/BLACKSTAR_RELEASE.md b/docs/BLACKSTAR_RELEASE.md index 22c96481..7de6ba29 100644 --- a/docs/BLACKSTAR_RELEASE.md +++ b/docs/BLACKSTAR_RELEASE.md @@ -1,14 +1,14 @@ # BlackSTAR Release Boundary -BlackSTAR `1.0.0` is an independently maintained successor derived from +BlackSTAR `1.1.0` is an independently maintained successor derived from upstream STAR `2.7.11b`. It preserves the qualified STAR compatibility and genome-format boundaries while adding genome-generation, persistent named-sequence insertion, and high-thread alignment improvements. The transitional executable lineage token is `2.7.11b-blackstar.3`. -This document describes the `v1.0.0` release boundary. Changes and measurements -explicitly labeled `Unreleased` or Q02 Labs evidence are excluded until a later -candidate passes release qualification and receives its own immutable tag. +This document describes the `v1.1.0` release boundary. Changes and measurements +explicitly labeled `Unreleased` remain excluded until a later candidate passes +release qualification and receives its own immutable tag. The qualified release target is x86-64 Linux. Release automation emits a baseline x86-64 artifact and an explicitly labeled AVX2 artifact. Inherited @@ -37,6 +37,14 @@ The original `blackstar.1` acceptance record is retained under inherited-policy preservation and shared-memory fallback. - Transcript-recursion copy elision that copies state only for mutating and terminal branches. +- Record-safe SAM-input chunk sizing and cgroup-aware memory assessment. +- Restoration of inherited NUMA policy after private genome loading. +- Isolated short-read and STARlong build state. +- Deterministic `TranscriptomeSAM` primary selection across worker schedules, + while preserving the complete transcript alignment set and later inherited + random-stream position. +- Explicit baseline x86-64 and AVX2 release variants with result-equivalence + and ISA-floor validation. ## Explicitly Excluded Experiments diff --git a/docs/COMPATIBILITY.md b/docs/COMPATIBILITY.md index 3a7eddad..01fdc450 100644 --- a/docs/COMPATIBILITY.md +++ b/docs/COMPATIBILITY.md @@ -18,7 +18,7 @@ BlackSTAR tracks three separate identities: | STAR compatibility base | Upstream source release from which inherited behavior is evaluated | | Genome format | `versionGenome` accepted when loading a conventional index | -For BlackSTAR `1.0.0`, the executable lineage token is +For BlackSTAR `1.1.0`, the executable lineage token is `2.7.11b-blackstar.3`. Its compatibility base is official STAR `2.7.11b` at commit `b1edc1208d91a53bf40ebae8669f71d50b994851`. The conventional genome format remains `2.7.4a`. @@ -86,9 +86,9 @@ Changes caused by explicit BlackSTAR-only references are expected. In a base-versus-Delta test, noninserted biological output must remain equivalent outside mappings and counts attributable to requested added references. -### Unreleased TranscriptomeSAM Correction +### TranscriptomeSAM Primary Determinism -The Q02 Labs candidate makes the `TranscriptomeSAM` primary-transcript flag +BlackSTAR 1.1.0 makes the `TranscriptomeSAM` primary-transcript flag deterministic from `runRNGseed` and the stable input-read ordinal. Official STAR uses a worker-local random generator for this choice, so a controlled run on the same reads produced different raw primary flags at 1 and 96 threads. @@ -99,9 +99,9 @@ flag `0x100` to a different member of an otherwise identical transcript alignment set than one particular official STAR run. One inherited random draw is retained per read so later inherited random choices do not shift. -This correction is present only in the unreleased Labs candidate documented by -[Q02](experiments/Q02-cross-workload-generalization.md). It is not a promise of -the current stable release until deliberately promoted and versioned. +The implementation and cross-thread oracle are documented by +[Q02](experiments/Q02-cross-workload-generalization.md) and the 1.1.0 +acceptance record. ## Resource and Runtime Behavior diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index 5440d7e7..517c39e0 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -75,12 +75,12 @@ All 57 cold-versus-warm artifact checks passed. Full and Delta alignment matched timing-independent metrics, junctions, gene counts, and canonical BAM records, with exactly 100 GFP and 100 GST fragments counted in each mode. -## Unreleased Cross-Workload Labs Evidence +## BlackSTAR 1.1.0 Cross-Workload Qualification -Q02 compared official STAR 2.7.11b with an unreleased BlackSTAR hardening -candidate across common modes beyond paired gene-count-only alignment. These -results are not part of the current release boundary and are not incremental -effects of Q02 alone; most reflect the cumulative BlackSTAR runtime stack. +Q02 compared official STAR 2.7.11b with the BlackSTAR 1.1.0 hardening source +across common modes beyond paired gene-count-only alignment. These are +cumulative workload results, not incremental effects of the 1.1.0 hardening +changes alone; most reflect the complete BlackSTAR runtime stack. | Workload | Official STAR | BlackSTAR | Median paired change | 95% interval | Qualification | | --- | ---: | ---: | ---: | ---: | --- | @@ -124,8 +124,8 @@ Bounded machine-readable receipts are committed under: - `docs/benchmarks/official-star-2.7.11b-vs-blackstar.2/` for the stable cumulative comparison; and -- `docs/benchmarks/Q02-cross-workload-20260726/` for the unreleased - cross-workload Labs candidate. +- `docs/benchmarks/Q02-cross-workload-20260726/` for the 1.1.0 + cross-workload qualification source. Large raw outputs remain outside Git. The receipts identify measured binaries and inputs and preserve the pair data, aggregate values, negative evidence, and diff --git a/docs/VERSIONING.md b/docs/VERSIONING.md index 840a11eb..ceefe1b8 100644 --- a/docs/VERSIONING.md +++ b/docs/VERSIONING.md @@ -7,7 +7,7 @@ machine-readable STAR ancestry and genome-format identities. | Surface | Example | Purpose | | --- | --- | --- | -| BlackSTAR release | `1.0.0` | Independent API, support, and release boundary | +| BlackSTAR release | `1.1.0` | Independent API, support, and release boundary | | STAR compatibility base | `2.7.11b` | Pinned inherited behavior oracle | | BlackSTAR lineage identity | `2.7.11b-blackstar.3` | Transitional executable identity | | Genome format | `2.7.4a` | Conventional index loading compatibility | diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 567f5fa0..29ac29fb 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -35,6 +35,7 @@ customer-derived evidence belong in an untracked internal derivative. 26. [Governance and release path](diagrams/svg/F26-governance-release.svg) 27. [Q02 cross-workload generalization](diagrams/svg/F27-cross-workload-generalization.svg) 28. [Transcriptome primary determinism](diagrams/svg/F28-transcriptome-primary-determinism.svg) +29. [BlackSTAR 1.1.0 qualification and release path](diagrams/svg/F29-v1.1-release-qualification.svg) The editable sources are under `diagrams/src/`. The generated SVG and PDF exports are presentation-ready but are never the source of truth. diff --git a/docs/architecture/STATUS_HISTORY.md b/docs/architecture/STATUS_HISTORY.md index 555a7826..89a93f87 100644 --- a/docs/architecture/STATUS_HISTORY.md +++ b/docs/architecture/STATUS_HISTORY.md @@ -24,8 +24,8 @@ not be described as shipped behavior. | A06 transcript recursion copy elision | Accepted | blackstar.2 | Exact commit 6032393 improved the five-pair uncompressed full-corpus median by 2.29% over A05 with a positive paired interval, exact outputs, compressed-input support, a canonical BAM pass, and Q01 cumulative qualification. | | A09 LTO and PGO toolchain variants | Rejected | None | Independent five-pair tests improved the cumulative A06 median by 1.21% with LTO and 1.20% with PGO. Both preserved exact measured outputs and had positive paired intervals, but neither met the 2% practical gate. | | Q01 cumulative alignment stack | Release qualification | blackstar.2 | H01+A02+A05+A06 produced median paired improvements of 33.20% uncompressed and 28.54% through zcat across three balanced pairs, lowered RSS, preserved exact outputs, and passed shared-index, BAM, affinity, sanitizer, insertion, SAindex, upstream-compatibility, selector, and reproducible-package gates. | -| Q02 cross-workload generalization | Complete mixed Labs result | None | Nine public mode series passed compatibility and noninferiority. All 36/36 pair-level output comparisons passed, but the exclusive-node single-end aggregate exceeded the variability gate and does not support a speed claim. | -| TranscriptomeSAM primary selection | Unreleased hardening candidate | None | Official STAR primary flags changed between controlled 1- and 96-thread runs. BlackSTAR made the raw flags exact across thread counts while preserving transcript alignment sets, genomic BAM records, counts, junctions, and later inherited RNG stream position. | +| Q02 cross-workload generalization | Accepted compatibility qualification with mixed performance evidence | 1.1.0 | All 36/36 pair-level output comparisons passed. The hardening source entered 1.1.0, while the exclusive-node single-end variability failure remains and does not support a speed claim. | +| TranscriptomeSAM primary selection | Accepted deterministic correction | 1.1.0 | Official STAR primary flags changed between controlled 1- and 96-thread runs. BlackSTAR made the raw flags exact across thread counts while preserving transcript alignment sets, genomic BAM records, counts, junctions, and later inherited RNG stream position. | | Early 41 percent full-index claim | Superseded | None | Replaced by three-pair 49.48 percent release evidence. | | Early Delta build and runtime figures | Superseded | None | Replaced by hardened package and promotion-gate evidence. | diff --git a/docs/architecture/claims.tsv b/docs/architecture/claims.tsv index e3cb5c76..5316df12 100644 --- a/docs/architecture/claims.tsv +++ b/docs/architecture/claims.tsv @@ -176,3 +176,9 @@ LAB-Q02-029 Q02 current-source focused sanitizer scripts 11/11 scripts affinity, LAB-Q02-031 Q02 superseded shared-host single-end point estimate 22.5487 percent five-pair single-end series with 7.6146 and 6.4056 percent arm CV docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 rejected and superseded; must not be cited as a speed gain LAB-Q02-032 Q02 32-thread single-end median paired result -1.9858 percent public single-end 150-base reads; five order-balanced pairs docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 no gain and 2 percent noninferiority not established; prevents an all-thread-count claim LAB-Q02-033 Q02 inherited STARlong default seed limit fail status public direct-RNA fixture under default seedPerReadNmax docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv 4dfe10bfe8b5b44d5fbef24baf9b952da4811baf5843576d749226795a75b353 76b7c2bdf63d54ef6ca7eaebe9fa65b05b506a28 both official and BlackSTAR STARlong abort; accepted performance comparison used symmetric 100000 override +RCQ-1.1-001 Exact BlackSTAR 1.1.0 source candidate local gate ledger 29/29 gates dedicated x86-64 Linux node; two clean roots; cumulative release qualification docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 b10c14c6515b62f3e730c4e25a3b6dca20caa508 all locally recorded gates passed; protected CI and tagged release gates remain separate +RCQ-1.1-002 Exact source candidate specialized compatibility matrix 12/12 checks official STAR differential modes including paired, transcriptome, counts, two-pass, BySJout, chimeric, WASP, STARsolo, SAM input, shared lifecycle, and transformed genome docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 b10c14c6515b62f3e730c4e25a3b6dca20caa508 all mode-specific differential checks passed +RCQ-1.1-003 Exact source candidate focused sanitizer matrix 11/11 scripts affinity, NUMA, chunks, transcriptome primary, memory, packed arrays, suffix comparison, transcript initialization, parameters, junctions, and SHA-256 docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 b10c14c6515b62f3e730c4e25a3b6dca20caa508 all focused ASan and UBSan scripts passed +RCQ-1.1-004 Exact source candidate GenomeInsert hardening matrix 32/32 subchecks Full, Overlay, Delta, GTF, namespace, relocation, corruption, cross-thread idempotence, and stock STAR compatibility docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 b10c14c6515b62f3e730c4e25a3b6dca20caa508 all GenomeInsert hardening subchecks passed +RCQ-1.1-005 Exact source candidate full CHM13 plus ERCC substantive index identity 14/14 artifacts one cumulative correctness run with retained Genome, SA, SAindex, chromosome, junction, gene, transcript, and exon artifacts docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 b10c14c6515b62f3e730c4e25a3b6dca20caa508 byte identity passed; single-run timing is not a speed claim +RCQ-1.1-006 Exact source candidate pre-release architecture reproduction 28/28 figures pinned Mermaid renderer before adding the release-qualification figure docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 b10c14c6515b62f3e730c4e25a3b6dca20caa508 all pre-release architecture SVG outputs reproduced exactly diff --git a/docs/architecture/diagrams/pdf/F25-version-compatibility.pdf b/docs/architecture/diagrams/pdf/F25-version-compatibility.pdf index 365a6870dd588cde6d6b89c8f524c89e2f096c38..6be242625a8043ef37ff471fd4df6c794501d4a3 100644 GIT binary patch delta 3241 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 diff --git a/docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf b/docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf index dea7b3d94d498cf94e520d68616ea5174e283692..947c001c789e1ff5908e1030aacfd063252a4913 100644 GIT binary patch delta 20924 zcmaI618^Ws^ynMgb~d(cY&O`~*x0tS$;8&iwr$(CZJQgv{oh;l>ej3Kz4}d^sXph_ zbai*lnKOO5;u6A>)_J_^YLU(LJ!qXyjr7RVp$KE8;v<=OuNU!TRfz{?|XarGy8 zztHE~!=m2j%l)>`+oPV)C+F{vNA+!?&zF7Qey;7`fB&vb4N`i1LwkyUcYZxTtuEG+ z)3G%8!7LX*-l_<{Q^xKaIs&2u*Dih>B+KV=Dd_#>P8tVh+S*3S&2I;%ge#aGS_h^M zlv?Cy=NjS(W`AZ⁣bUzS+SW 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 diff --git a/docs/architecture/diagrams/pdf/F29-v1.1-release-qualification.pdf b/docs/architecture/diagrams/pdf/F29-v1.1-release-qualification.pdf new file mode 100644 index 0000000000000000000000000000000000000000..396cb4eed7fa9bce5a10027f412fafa0a983aae5 GIT binary patch literal 33830 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 literal 0 HcmV?d00001 diff --git a/docs/architecture/diagrams/src/F25-version-compatibility.mmd b/docs/architecture/diagrams/src/F25-version-compatibility.mmd index 6cb8c08c..4cd38b13 100644 --- a/docs/architecture/diagrams/src/F25-version-compatibility.mmd +++ b/docs/architecture/diagrams/src/F25-version-compatibility.mmd @@ -1,5 +1,5 @@ flowchart TB - RELEASE["BlackSTAR release
1.0.0
support and API boundary"] + RELEASE["BlackSTAR release
1.1.0
support and API boundary"] EXEC["Executable lineage token
2.7.11b-blackstar.3
wrapper-compatible legacy output"] BASE["STAR compatibility base
2.7.11b at pinned commit
behavioral oracle"] FORMAT["Genome format
versionGenome 2.7.4a
conventional index loading"] diff --git a/docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd b/docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd index e2839a6f..eba35649 100644 --- a/docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd +++ b/docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd @@ -1,6 +1,6 @@ flowchart TB CONTROL["OFFICIAL STAR 2.7.11b
PINNED CONTROL"] --> MATRIX["Q02 PUBLIC WORKLOAD MATRIX
96 threads; local storage; balanced pairs"] - CANDIDATE["UNRELEASED BLACKSTAR LABS CANDIDATE
blackstar.2 stack plus hardening"] --> MATRIX + CANDIDATE["BLACKSTAR 1.1.0 HARDENING SOURCE
blackstar.2 stack plus Q02 corrections"] --> MATRIX MATRIX --> BULK["FRAGMENTED AND SPECIALIZED BULK"] BULK --> PE76["Paired 76-base
20.05% measured gain
5/5 outputs exact"] @@ -27,7 +27,7 @@ flowchart TB LONG --> CORRECT SINGLE --> CORRECT - CORRECT --> DECISION["MIXED Q02 DECISION
retain hardening candidate
outside release boundary"] + CORRECT --> DECISION["MIXED PERFORMANCE DECISION
compatibility source accepted in 1.1.0
single-end speed claim rejected"] LIMIT["LIMITS
one CPU family; local storage; 96-thread primary series
STARlong requires symmetric seed-limit override
single-end timing remains unresolved"] -.-> DECISION classDef control fill:#f3f4f6,stroke:#4b5563,color:#111827; diff --git a/docs/architecture/diagrams/src/F29-v1.1-release-qualification.mmd b/docs/architecture/diagrams/src/F29-v1.1-release-qualification.mmd new file mode 100644 index 00000000..d2b66a4d --- /dev/null +++ b/docs/architecture/diagrams/src/F29-v1.1-release-qualification.mmd @@ -0,0 +1,32 @@ +flowchart TB + SOURCE["EXACT SOURCE CANDIDATE
b10c14c; Q02 runtime tree exact"] --> ROOTS["TWO INDEPENDENT CLEAN ROOTS
baseline and AVX2 release builds"] + ROOTS --> REPRO["CROSS-PATH REPRODUCIBILITY
release products byte-identical"] + + SOURCE --> LOCAL["DEDICATED-NODE CUMULATIVE GATES
29/29 recorded gates pass"] + LOCAL --> MODES["12 specialized modes
11 sanitizer scripts
STARlong and SAindex"] + LOCAL --> INSERT["32 GenomeInsert checks
stock STAR compatibility
GFP/GST Delta oracle"] + LOCAL --> WORKLOAD["12.77M paired reads exact
CHM13+ERCC 14/14 artifacts
28/28 architecture figures"] + + REPRO --> PREP["BLACKSTAR 1.1.0 RELEASE PREPARATION
version, records, architecture, CI policy"] + MODES --> PREP + INSERT --> PREP + WORKLOAD --> PREP + + PREP --> CI["PROTECTED REQUIRED CHECKS
GCC + Clang + Ubuntu 20.04 portability
STARlong + CodeQL + full acceptance"] + CI --> MAIN["PROTECTED MAIN
all checks successful"] + MAIN --> TAG["ANNOTATED v1.1.0 TAG
exact protected commit"] + TAG --> RELEASE["TWO TAGGED CLEAN BUILDS
compare + SBOM + attest + publish"] + + LIMITS["RETAINED LIMITS
no single-end speed claim
STARsolo and TranscriptomeSAM noninferior only
STARlong seed-limit caveat"] -.-> PREP + DEPLOY["EXTERNAL DEPLOYMENT
separate authorization"] -.-> RELEASE + + classDef source fill:#dbeafe,stroke:#1d4ed8,color:#111827; + classDef passed fill:#ecfdf5,stroke:#047857,color:#111827; + classDef gate fill:#f3f4f6,stroke:#4b5563,color:#111827; + classDef release fill:#dcfce7,stroke:#15803d,color:#111827,stroke-width:3px; + classDef limit fill:#fef3c7,stroke:#a16207,color:#111827,stroke-dasharray:5 5; + class SOURCE,PREP source; + class ROOTS,LOCAL,MODES,INSERT,WORKLOAD,REPRO passed; + class CI,MAIN,TAG gate; + class RELEASE release; + class LIMITS,DEPLOY limit; diff --git a/docs/architecture/diagrams/svg/F25-version-compatibility.svg b/docs/architecture/diagrams/svg/F25-version-compatibility.svg index 8550e94a..4676bc6b 100644 --- a/docs/architecture/diagrams/svg/F25-version-compatibility.svg +++ b/docs/architecture/diagrams/svg/F25-version-compatibility.svg @@ -1,2 +1,2 @@ - -

BlackSTAR release
1.0.0
support and API boundary

Executable lineage token
2.7.11b-blackstar.3
wrapper-compatible legacy output

STAR compatibility base
2.7.11b at pinned commit
behavioral oracle

Genome format
versionGenome 2.7.4a
conventional index loading

STAR --version-json
reports all identities

Inherited CLI and defaults

Conventional Full indexes

BlackSTAR-only Overlay and Delta

Compatibility gates
metrics + junctions + counts
canonical BAM + byte identity

Versions change independently;
no one string represents all contracts

+ +

BlackSTAR release
1.1.0
support and API boundary

Executable lineage token
2.7.11b-blackstar.3
wrapper-compatible legacy output

STAR compatibility base
2.7.11b at pinned commit
behavioral oracle

Genome format
versionGenome 2.7.4a
conventional index loading

STAR --version-json
reports all identities

Inherited CLI and defaults

Conventional Full indexes

BlackSTAR-only Overlay and Delta

Compatibility gates
metrics + junctions + counts
canonical BAM + byte identity

Versions change independently;
no one string represents all contracts

diff --git a/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg b/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg index 40aa83df..8363e8c5 100644 --- a/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg +++ b/docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg @@ -1,2 +1,2 @@ - -

OFFICIAL STAR 2.7.11b
PINNED CONTROL

Q02 PUBLIC WORKLOAD MATRIX
96 threads; local storage; balanced pairs

UNRELEASED BLACKSTAR LABS CANDIDATE
blackstar.2 stack plus hardening

FRAGMENTED AND SPECIALIZED BULK

Paired 76-base
20.05% measured gain
5/5 outputs exact

Paired 150-base
12.67% measured gain
3/3 outputs exact

Two-pass
20.25% measured gain
3/3 outputs exact

BySJout
31.45% measured gain
3/3 outputs exact

Chimeric
31.76% measured gain
3/3 outputs exact

Sorted BAM
18.42% measured gain
5/5 canonical BAM exact

OTHER COMMON MODES

TranscriptomeSAM
1.55% point estimate
accepted noninferiority

STARsolo 10x v3
3.78% point estimate
accepted noninferiority

STARlong direct RNA
19.48% measured gain
3/3 canonical SAM exact

Single-end 150-base
1.67% point estimate
CV gate failed; no speed claim

36/36 MODE-SPECIFIC
PAIR COMPARISONS PASSED

MIXED Q02 DECISION
retain hardening candidate
outside release boundary

LIMITS
one CPU family; local storage; 96-thread primary series
STARlong requires symmetric seed-limit override
single-end timing remains unresolved

+ +

OFFICIAL STAR 2.7.11b
PINNED CONTROL

Q02 PUBLIC WORKLOAD MATRIX
96 threads; local storage; balanced pairs

BLACKSTAR 1.1.0 HARDENING SOURCE
blackstar.2 stack plus Q02 corrections

FRAGMENTED AND SPECIALIZED BULK

Paired 76-base
20.05% measured gain
5/5 outputs exact

Paired 150-base
12.67% measured gain
3/3 outputs exact

Two-pass
20.25% measured gain
3/3 outputs exact

BySJout
31.45% measured gain
3/3 outputs exact

Chimeric
31.76% measured gain
3/3 outputs exact

Sorted BAM
18.42% measured gain
5/5 canonical BAM exact

OTHER COMMON MODES

TranscriptomeSAM
1.55% point estimate
accepted noninferiority

STARsolo 10x v3
3.78% point estimate
accepted noninferiority

STARlong direct RNA
19.48% measured gain
3/3 canonical SAM exact

Single-end 150-base
1.67% point estimate
CV gate failed; no speed claim

36/36 MODE-SPECIFIC
PAIR COMPARISONS PASSED

MIXED PERFORMANCE DECISION
compatibility source accepted in 1.1.0
single-end speed claim rejected

LIMITS
one CPU family; local storage; 96-thread primary series
STARlong requires symmetric seed-limit override
single-end timing remains unresolved

diff --git a/docs/architecture/diagrams/svg/F29-v1.1-release-qualification.svg b/docs/architecture/diagrams/svg/F29-v1.1-release-qualification.svg new file mode 100644 index 00000000..41fecbdc --- /dev/null +++ b/docs/architecture/diagrams/svg/F29-v1.1-release-qualification.svg @@ -0,0 +1,2 @@ + +

EXACT SOURCE CANDIDATE
b10c14c; Q02 runtime tree exact

TWO INDEPENDENT CLEAN ROOTS
baseline and AVX2 release builds

CROSS-PATH REPRODUCIBILITY
release products byte-identical

DEDICATED-NODE CUMULATIVE GATES
29/29 recorded gates pass

12 specialized modes
11 sanitizer scripts
STARlong and SAindex

32 GenomeInsert checks
stock STAR compatibility
GFP/GST Delta oracle

12.77M paired reads exact
CHM13+ERCC 14/14 artifacts
28/28 architecture figures

BLACKSTAR 1.1.0 RELEASE PREPARATION
version, records, architecture, CI policy

PROTECTED REQUIRED CHECKS
GCC + Clang + Ubuntu 20.04 portability
STARlong + CodeQL + full acceptance

PROTECTED MAIN
all checks successful

ANNOTATED v1.1.0 TAG
exact protected commit

TWO TAGGED CLEAN BUILDS
compare + SBOM + attest + publish

RETAINED LIMITS
no single-end speed claim
STARsolo and TranscriptomeSAM noninferior only
STARlong seed-limit caveat

EXTERNAL DEPLOYMENT
separate authorization

diff --git a/docs/architecture/figures.json b/docs/architecture/figures.json index bdebab78..0c15c25b 100644 --- a/docs/architecture/figures.json +++ b/docs/architecture/figures.json @@ -276,7 +276,7 @@ "visibility": "public", "code_commit": "9998c445c5b87adacd2a4663bd964ce744aea300", "source": "docs/architecture/diagrams/src/F23-blackstar2-promotion.mmd", - "evidence": ["docs/BLACKSTAR_RELEASE.md", "docs/BLACKSTAR_ACCEPTANCE.md", "docs/experiments/Q01-cumulative-alignment-qualification.md", "docs/architecture/evidence/release-2.7.11b-blackstar.2.tsv"], + "evidence": ["docs/releases/2.7.11b-blackstar.2-release-notes.md", "docs/releases/2.7.11b-blackstar.2-acceptance.md", "docs/experiments/Q01-cumulative-alignment-qualification.md", "docs/architecture/evidence/release-2.7.11b-blackstar.2.tsv"], "caption": "Blackstar.2 promotes the Q01-qualified H01, A02, A05, and A06 alignment stack on top of blackstar.1 while preserving protected-master CI, explicit exclusions, and a separate external-deployment boundary.", "alt_text": "Promotion diagram from upstream STAR 2.7.11b to the qualified blackstar.1 index and insertion release, then through H01 affinity recovery, A02 adaptive input chunks, A05 NUMA-aware private loading, A06 transcript-recursion copy elision, and Q01 cumulative qualification. Replicated performance, exact-output safety, inherited insertion and SAindex compatibility, versioned release packaging, and protected-master build-and-test gates lead to the accepted blackstar.2 release boundary. A01, A02b, A09, alignReadsMulti, prefork workers, and threaded BAM compression remain excluded, and external deployment requires separate authorization.", "outputs": ["docs/architecture/diagrams/svg/F23-blackstar2-promotion.svg", "docs/architecture/diagrams/pdf/F23-blackstar2-promotion.pdf"] @@ -296,13 +296,13 @@ { "id": "F25", "title": "Version and compatibility identities", - "maturity": "roadmap", + "maturity": "accepted", "visibility": "public", - "code_commit": "d6fbf932ae2b155ce4f689bce106429ab2bc07f6", + "code_commit": "b10c14c6515b62f3e730c4e25a3b6dca20caa508", "source": "docs/architecture/diagrams/src/F25-version-compatibility.mmd", "evidence": ["docs/VERSIONING.md", "docs/COMPATIBILITY.md", "source/VERSION", "source/Parameters.cpp"], "caption": "Independent BlackSTAR release version, executable lineage token, pinned STAR compatibility base, and genome-format version are separate identities exposed together through machine-readable version metadata.", - "alt_text": "Four-part identity diagram showing BlackSTAR release 1.0.0 as the support and API boundary, the wrapper-compatible executable token 2.7.11b-blackstar.3, official STAR 2.7.11b as a pinned behavioral oracle, and genome format 2.7.4a for conventional index loading. All four feed version JSON, while inherited CLI, Full indexes, and BlackSTAR-only Overlay and Delta paths feed explicit compatibility tests.", + "alt_text": "Four-part identity diagram showing BlackSTAR release 1.1.0 as the support and API boundary, the wrapper-compatible executable token 2.7.11b-blackstar.3, official STAR 2.7.11b as a pinned behavioral oracle, and genome format 2.7.4a for conventional index loading. All four feed version JSON, while inherited CLI, Full indexes, and BlackSTAR-only Overlay and Delta paths feed explicit compatibility tests.", "outputs": ["docs/architecture/diagrams/svg/F25-version-compatibility.svg", "docs/architecture/diagrams/pdf/F25-version-compatibility.pdf"] }, { @@ -320,19 +320,19 @@ { "id": "F27", "title": "Q02 cross-workload generalization", - "maturity": "experimental", + "maturity": "accepted", "visibility": "public", "code_commit": "ef2a2560013293a3cd93403d876f50a3d5ec759c", "source": "docs/architecture/diagrams/src/F27-cross-workload-generalization.mmd", "evidence": ["docs/experiments/Q02-cross-workload-generalization.md", "docs/architecture/evidence/alignment-Q02-cross-workload-20260726.tsv"], "caption": "Q02 extends direct official-STAR comparisons across fragmented paired, single-end, two-pass, junction, chimeric, BAM, single-cell, transcriptome, and long-read modes while retaining the failed single-end timing gate.", - "alt_text": "Cross-workload qualification diagram comparing official STAR 2.7.11b with the unreleased BlackSTAR Labs candidate at 96 threads. Paired 76-base, paired 150-base, two-pass, BySJout, chimeric, coordinate-sorted BAM, and STARlong modes show positive measured intervals with every applicable output comparison passing. TranscriptomeSAM and STARsolo pass noninferiority without a speed claim. Single-end preserves outputs and noninferiority but fails its variability gate. Thirty-six of thirty-six mode-specific pair comparisons pass, leading to a mixed decision that retains the hardening candidate outside the release boundary.", + "alt_text": "Cross-workload qualification diagram comparing official STAR 2.7.11b with the BlackSTAR 1.1.0 hardening source at 96 threads. Paired 76-base, paired 150-base, two-pass, BySJout, chimeric, coordinate-sorted BAM, and STARlong modes show positive measured intervals with every applicable output comparison passing. TranscriptomeSAM and STARsolo pass noninferiority without a speed claim. Single-end preserves outputs and noninferiority but fails its variability gate. Thirty-six of thirty-six mode-specific pair comparisons pass; compatibility hardening is accepted in 1.1.0 while the single-end speed claim remains rejected.", "outputs": ["docs/architecture/diagrams/svg/F27-cross-workload-generalization.svg", "docs/architecture/diagrams/pdf/F27-cross-workload-generalization.pdf"] }, { "id": "F28", "title": "Transcriptome primary determinism", - "maturity": "experimental", + "maturity": "accepted", "visibility": "public", "code_commit": "ef2a2560013293a3cd93403d876f50a3d5ec759c", "source": "docs/architecture/diagrams/src/F28-transcriptome-primary-determinism.mmd", @@ -340,6 +340,18 @@ "caption": "BlackSTAR replaces worker-dependent TranscriptomeSAM primary selection with a run-seed and read-ordinal selector while retaining the legacy RNG draw and preserving the complete upstream transcript alignment set.", "alt_text": "Before-and-after determinism diagram. Official STAR routes a stable input read ordinal to workers whose chunk-seeded local random generators choose transcript primary flags; a controlled one-versus-96-thread test produces different raw primary digests. BlackSTAR hashes the run seed and stable read ordinal with SplitMix64, making the choice independent of worker scheduling; the same test produces exact raw digests. One legacy random draw is retained to preserve later random-stream position. Transcript alignment sets after clearing only flag 0x100, genomic BAM records, gene counts, junctions, and timing-independent metrics remain exact.", "outputs": ["docs/architecture/diagrams/svg/F28-transcriptome-primary-determinism.svg", "docs/architecture/diagrams/pdf/F28-transcriptome-primary-determinism.pdf"] + }, + { + "id": "F29", + "title": "BlackSTAR 1.1.0 qualification and release path", + "maturity": "accepted", + "visibility": "public", + "code_commit": "b10c14c6515b62f3e730c4e25a3b6dca20caa508", + "source": "docs/architecture/diagrams/src/F29-v1.1-release-qualification.mmd", + "evidence": ["docs/BLACKSTAR_ACCEPTANCE.md", "docs/RELEASE_POLICY.md", "docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv"], + "caption": "BlackSTAR 1.1.0 promotion requires exact-candidate cumulative qualification, seven protected GitHub checks, a protected-main tag, two byte-identical tagged builds, and attested immutable publication.", + "alt_text": "Release qualification diagram starting from exact source candidate b10c14c. Two independent clean roots produce byte-identical baseline and AVX2 artifacts, while 29 dedicated-node gates cover specialized modes, sanitizers, STARlong, SAindex, GenomeInsert, public paired reads, GFP and GST Delta mapping, full CHM13 plus ERCC indexes, and architecture rendering. Versioned release preparation then enters seven protected GitHub checks, protected main, an annotated v1.1.0 tag, two tagged clean builds, artifact comparison, SBOM generation, attestation, and publication. Single-end, STARsolo, TranscriptomeSAM, STARlong, and external-deployment limitations remain visible.", + "outputs": ["docs/architecture/diagrams/svg/F29-v1.1-release-qualification.svg", "docs/architecture/diagrams/pdf/F29-v1.1-release-qualification.pdf"] } ] } diff --git a/docs/benchmarks/RCQ-1.1.0-source-20260726/README.md b/docs/benchmarks/RCQ-1.1.0-source-20260726/README.md new file mode 100644 index 00000000..d1bcc4f6 --- /dev/null +++ b/docs/benchmarks/RCQ-1.1.0-source-20260726/README.md @@ -0,0 +1,46 @@ +# BlackSTAR 1.1.0 Source-Candidate Qualification + +This bounded public receipt records the dedicated-node qualification of the +source candidate later prepared for BlackSTAR 1.1.0. + +The exact candidate was +`b10c14c6515b62f3e730c4e25a3b6dca20caa508`. Its code and test-harness tree +was exact to Q02 runtime source +`ef2a2560013293a3cd93403d876f50a3d5ec759c`; later files at the candidate were +documentation. Release preparation after this candidate is restricted to +version identity, release records, architecture status, CI package-path +generalization, and required-check policy. + +## Verdict + +All 29 locally recorded gates passed on a dedicated x86-64 Linux Slurm node. +This source-candidate receipt does not replace protected-branch CI or tagged +release-artifact qualification. + +## Covered Gates + +- Two independent clean release roots and byte-identical baseline and AVX2 + products. +- Release identity, OpenMP linkage, ISA labels, and cross-variant output + equivalence. +- 12 specialized official-STAR differential modes. +- 11 focused ASan and UBSan scripts. +- 32 GenomeInsert hardening subchecks, including stock-STAR full-index + compatibility. +- Serial, bounded-parallel, and constrained-memory SAindex identity. +- STARlong build and official-STARlong smoke comparison. +- Public paired-read correctness, real GFP/GST Delta equivalence, and full + CHM13+ERCC substantive-index identity. +- Deterministic architecture rendering. + +The full-index and paired-read timings were single-run descriptive receipts, +not replacement speed claims. The Q02 package remains authoritative for +replicated cross-workload performance evidence. + +## Files + +- `qualification-receipt.tsv`: bounded identities and outcomes. +- `SHA256SUMS`: integrity manifest for this public receipt. + +Large source checkouts, binaries, indexes, alignments, and raw logs are not +tracked. diff --git a/docs/benchmarks/RCQ-1.1.0-source-20260726/SHA256SUMS b/docs/benchmarks/RCQ-1.1.0-source-20260726/SHA256SUMS new file mode 100644 index 00000000..7261440c --- /dev/null +++ b/docs/benchmarks/RCQ-1.1.0-source-20260726/SHA256SUMS @@ -0,0 +1,2 @@ +6b2f9d092743e941de7f7444f9a1fee056bf06e25dea9b6622984a8304b50822 README.md +c7e522363046a5d3e4d12c3f6f59bc7ef731da727ce3925005ea447b3f8c1015 qualification-receipt.tsv diff --git a/docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv b/docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv new file mode 100644 index 00000000..1035cec5 --- /dev/null +++ b/docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv @@ -0,0 +1,34 @@ +field value qualification source +schema blackstar-source-candidate-qualification-v1 exact local qualification procedure +candidate_commit b10c14c6515b62f3e730c4e25a3b6dca20caa508 exact git +runtime_source_commit ef2a2560013293a3cd93403d876f50a3d5ec759c code and test-harness tree exact git diff +host_class dedicated dual-socket x86-64 Slurm node exclusive during qualification system inventory +start_utc 2026-07-26T21:22:18Z exact system clock +finish_utc 2026-07-26T21:47:55Z includes supplemental stock-STAR gate system clock +local_gate_ledger 29/29 all recorded gates passed gate ledger +candidate_source_archive_sha256 293d88dae647a43128dcc12b830aeb33a312aaf17f3e7403c018f2e26e6bf79f exact commit archive source receipt +candidate_baseline_binary_sha256 2f3e29293d2107ce40c93d12648dadb2c6dbdf1ec2d333c41f1dc809e647ec0c exact qualification binary build receipt +candidate_avx2_binary_sha256 334e172ae963150e7dac81e8b996afacedaa460daab00f4695e29a0bcc4c0c49 exact qualification binary build receipt +cross_root_release_reproducibility PASS baseline and AVX2 products exact across two paths build comparison +release_identity_openmp_isa PASS baseline and AVX2 identities linkage and ISA labels exact binary inspection +release_variant_result_equivalence PASS baseline and AVX2 measured outputs exact differential test +specialized_mode_matrix 12/12 official STAR differential oracle specialized test +focused_asan_ubsan 11/11 all focused sanitizer scripts passed focused tests +deployment_selector PASS pinned selection rollback and negative cases selector test +genome_insert_hardening 32/32 includes stock STAR compatibility and cross-thread idempotence genomeInsert test +saindex_strategy_equivalence PASS serial bounded-parallel and constrained paths exact SAindex test +starlong PASS build long-read smoke chunk floor and upstream equivalence STARlong test +public_paired76_correctness PASS 12768316 pairs metrics junctions and gene counts exact public fixture +real_gfp_gst_delta_package_identity 5/5 inserted FASTA GTF overlay delta and manifest exact real Delta fixture +real_gfp_gst_mapping_equivalence PASS metrics junctions counts and canonical BAM exact real Delta fixture +real_gfp_gst_counts 100 / 100 GFP / GST fragments gene-count oracle +full_chm13_ercc_index_identity 14/14 all retained substantive artifacts exact full-index oracle +architecture_render_reproducibility 28/28 pinned Mermaid SVG outputs exact render check +performance_classification descriptive only single-run RCQ timings are not speed claims release policy +publication_status source candidate local qualification does not replace protected CI or tagged artifacts release policy +claim_id_RCQ_001 RCQ-1.1-001 29/29 locally recorded gates passed this receipt +claim_id_RCQ_002 RCQ-1.1-002 12/12 specialized differential modes passed this receipt +claim_id_RCQ_003 RCQ-1.1-003 11/11 focused sanitizer scripts passed this receipt +claim_id_RCQ_004 RCQ-1.1-004 32/32 GenomeInsert hardening subchecks passed this receipt +claim_id_RCQ_005 RCQ-1.1-005 14/14 retained CHM13 plus ERCC index artifacts matched this receipt +claim_id_RCQ_006 RCQ-1.1-006 28/28 pre-release architecture figures reproduced this receipt diff --git a/docs/experiments/Q02-cross-workload-generalization.md b/docs/experiments/Q02-cross-workload-generalization.md index 90030b10..1a5403a1 100644 --- a/docs/experiments/Q02-cross-workload-generalization.md +++ b/docs/experiments/Q02-cross-workload-generalization.md @@ -2,14 +2,15 @@ ## Status -- State: complete; mixed qualification result +- State: complete; mixed performance result and accepted compatibility source - Parent commit: `0978542f0957c47548f5f00fc706e1e08c021375` - Final experiment commit: `ef2a2560013293a3cd93403d876f50a3d5ec759c` - Opened: 2026-07-25 - Decided: 2026-07-26 -- Release disposition: retained as an unreleased Labs hardening candidate +- Release disposition: promoted by the separate BlackSTAR 1.1.0 release + qualification, with the negative timing evidence retained Q02 asked whether BlackSTAR's improvements and compatibility controls extend beyond the paired short-read, gene-count-only workload that originally drove @@ -60,8 +61,9 @@ benchmark infrastructure, not the earlier measured FASTQ runtime paths. Performance values nevertheless remain claims about the exact measured binary, not an unmeasured future release artifact. -This phase does not authorize a version bump, tag, GitHub update, release, or -external pipeline deployment. +Q02 alone did not authorize a version bump, tag, GitHub update, release, or +external pipeline deployment. A later exact-candidate release qualification +accepted the compatibility hardening for BlackSTAR 1.1.0. ## Correctness Contract @@ -214,14 +216,16 @@ are not expected to resolve in a fresh clone. ## Decision -- Outcome: retain the hardening source and evidence as a Labs candidate. +- Outcome: retain the mixed performance decision and accept the hardening + source through the separate 1.1.0 release qualification. - Compatibility: no measured biological-output regression across the tested short-read, single-cell, specialized, and long-read modes. - Performance: strong cumulative gains generalize to several fragmented and specialized modes; STARsolo and TranscriptomeSAM are noninferior only. - Unresolved: single-end performance remains variable and does not qualify as an improvement. -- Release: not promoted, versioned, tagged, pushed, or deployed by Q02. +- Release: promoted in BlackSTAR 1.1.0 after cumulative exact-candidate gates; + Q02 itself did not authorize publication or deployment. - Follow-up: any release candidate must rebuild from the eventual protected commit and rerun release reproducibility plus the selected cumulative workload matrix. diff --git a/docs/experiments/README.md b/docs/experiments/README.md index c1ae2434..a3cfda21 100644 --- a/docs/experiments/README.md +++ b/docs/experiments/README.md @@ -1,8 +1,10 @@ # BlackSTAR Labs Experiments -Labs experiments are isolated from the qualified release. Every experiment has -a stable ID, a frozen parent commit, a falsifiable hypothesis, correctness and -resource gates, raw evidence locations, diagrams, and an explicit decision. +Labs experiments are isolated investigations. A change enters the qualified +release only through a separate cumulative acceptance and publication path. +Every experiment has a stable ID, a frozen parent commit, a falsifiable +hypothesis, correctness and resource gates, raw evidence locations, diagrams, +and an explicit decision. ## Required Lifecycle @@ -60,6 +62,8 @@ claim an ordinary-path speedup. - [Q02](Q02-cross-workload-generalization.md): complete cross-workload hardening and compatibility record; nine public series passed, while the exclusive-node single-end timing series retained a variability-gate failure. + The hardening source was later accepted in BlackSTAR 1.1.0 without converting + that failure into a speed claim. Its bounded receipts are tracked under [`docs/benchmarks/Q02-cross-workload-20260726/`](../benchmarks/Q02-cross-workload-20260726/README.md). diff --git a/docs/experiments/ROADMAP.md b/docs/experiments/ROADMAP.md index 90d6aed5..871adf19 100644 --- a/docs/experiments/ROADMAP.md +++ b/docs/experiments/ROADMAP.md @@ -1,10 +1,10 @@ # BlackSTAR Labs Experimental Roadmap -The current qualified release is `2.7.11b-blackstar.2`. It inherits -`2.7.11b-blackstar.1` at -`821457378fa38bfb23b061b8f11ee0a09431dda7` and promotes the Q01-qualified -H01+A02+A05+A06 alignment stack. Later Labs work remains outside the release -until it passes cumulative qualification and is deliberately promoted. +The current qualified release is BlackSTAR `1.1.0`. It retains the +Q01-qualified H01+A02+A05+A06 alignment stack and promotes the Q02 +compatibility hardening after separate cumulative release qualification. Later +Labs work remains outside the release until it passes cumulative qualification +and is deliberately promoted. | ID | Area | Hypothesis | State | Dependency | | --- | --- | --- | --- | --- | @@ -21,7 +21,7 @@ until it passes cumulative qualification and is deliberately promoted. | A08 | Multi-sample scheduler | Shared immutable index memory plus explicit resource tokens improves node throughput with complete isolation. | Proposed | A07 | | A09 | Toolchain | LTO and profile-guided optimization improve the accepted cumulative alignment stack without semantic changes. | Rejected; LTO and PGO each gained about 1.2%, below the 2% practical gate | A06 | | Q01 | Cumulative alignment qualification | The complete H01+A02+A05+A06 stack preserves release behavior and generalizes across private, shared, compressed, BAM, affinity, sanitizer, compatibility, and package gates. | Complete; promoted in blackstar.2 | A06 and A09 decision | -| Q02 | Cross-workload generalization | The released high-thread stack and new compatibility hardening preserve behavior beyond paired gene-count-only RNA-seq. | Complete; nine public series passed, single-end timing remains unresolved, no release promotion | Q01 and successor transition | +| Q02 | Cross-workload generalization | The released high-thread stack and new compatibility hardening preserve behavior beyond paired gene-count-only RNA-seq. | Complete; compatibility hardening accepted in 1.1.0, single-end speed remains unresolved | Q01 and successor transition | | S01 | STARsolo post-mapping parallelism | Phase-resolved profiling may identify deterministic parallelism in barcode aggregation, Solo-record ingestion, per-cell UMI collapse, cell filtering, and matrix output. | Deferred future opportunity; no implementation scheduled | Q02 and dedicated STARsolo profile | | I01 | Genome preparation | Parallel reverse-complement and bounded private prefix histograms reduce serial setup. | Proposed | Cumulative alignment qualification | | I02 | SA packing | Record-block partitioning permits deterministic disjoint-byte parallel packing. | Proposed | I01 | @@ -61,7 +61,8 @@ belong in the cumulative gate. - Q01 pre-promotion package artifacts retain the old `.1` source version and are not release assets. Only clean `.2` packages from the protected release commit are distributable. -- Q02 does not establish a single-end speed improvement. Its exclusive-node +- Q02 does not establish a single-end speed improvement. BlackSTAR 1.1.0 + retains this limitation: its exclusive-node five-pair series passed correctness, RSS, and noninferiority but exceeded the 5 percent compatibility CV threshold. - Q02 TranscriptomeSAM output intentionally stabilizes the primary transcript diff --git a/docs/releases/1.1.0-release-notes.md b/docs/releases/1.1.0-release-notes.md new file mode 100644 index 00000000..84e2d9cd --- /dev/null +++ b/docs/releases/1.1.0-release-notes.md @@ -0,0 +1,52 @@ +# BlackSTAR 1.1.0 + +BlackSTAR 1.1.0 broadens the qualified BlackSTAR runtime beyond the original +paired short-read workflow and hardens release portability, memory policy, and +named-sequence insertion. + +## Highlights + +- Adds deterministic `TranscriptomeSAM` primary selection across thread + schedules while preserving the complete transcript alignment set, genomic + BAM records, counts, junctions, and inherited downstream random state. +- Makes automatic index strategies honor cgroup-aware memory limits. +- Restores inherited NUMA policy after eligible private genome loading. +- Hardens SAM-input chunk sizing and genome-insert annotation, namespace, + package-identity, relocation, and corruption checks. +- Isolates `STAR` and `STARlong` build state and qualifies a real direct-RNA + STARlong fixture. +- Publishes separately labeled baseline x86-64 and AVX2 packages, with explicit + ISA inspection and output-equivalence tests. + +## Cross-Workload Qualification + +The Q02 matrix compared BlackSTAR with official STAR 2.7.11b across fragmented +paired and single-end RNA-seq, two-pass mapping, BySJout, chimeric detection, +coordinate-sorted BAM, transcriptome BAM, STARsolo, STARlong, SAM input, WASP, +shared-memory lifecycle, and transformed-genome output. + +All 36 public pair-level output comparisons and all 12 specialized differential +checks passed. Replicated gains were observed for paired 150-base reads, +BySJout, chimeric detection, and STARlong. Other qualified rows preserve their +published variability and claim boundaries. + +## Limitations + +- The single-end timing series preserved outputs and passed noninferiority but + exceeded its variability gate; BlackSTAR makes no single-end speed claim. +- STARsolo and TranscriptomeSAM were noninferior, not demonstrably faster. +- The accepted STARlong fixture requires the same raised `seedPerReadNmax` in + both official STAR and BlackSTAR; both inherit a failure under the default. +- Performance evidence covers x86-64 Linux, local storage, the recorded + fixtures, and primarily high thread counts. It is not a universal guarantee. +- Overlay and Delta remain BlackSTAR-specific and require + `--genomeLoad NoSharedMemory`. + +## Compatibility + +The compatibility base remains official STAR 2.7.11b and conventional genome +format `2.7.4a`. The legacy `STAR --version` token remains +`2.7.11b-blackstar.3`; `STAR --version-json` reports BlackSTAR `1.1.0`. + +Release publication does not authorize integration into an external production +pipeline. diff --git a/docs/releases/2.7.11b-blackstar.2-acceptance.md b/docs/releases/2.7.11b-blackstar.2-acceptance.md new file mode 100644 index 00000000..2c3ed21e --- /dev/null +++ b/docs/releases/2.7.11b-blackstar.2-acceptance.md @@ -0,0 +1,189 @@ +# BlackSTAR 2.7.11b-blackstar.2 Acceptance Record + +## Verdict + +BlackSTAR `2.7.11b-blackstar.2` has passed the technical gates for an x86-64 +Linux release. It inherits the accepted index-generation, genome-insert, +integrity, and deployment boundary from `2.7.11b-blackstar.1` and adds the +cumulatively qualified H01, A02, A05, and A06 alignment changes. + +The cumulative qualification record is commit +`9998c445c5b87adacd2a4663bd964ce744aea300`. Paired timing used cumulative +source revision `7b31a5fe5cb966c9146b99d5ca1ad81ea9c81cdb`; the final runtime +implementation commit is `6032393155317b17fef1750672f1da6770ae6042`. +The benchmark harness revision is +`9d37e0f8009c28fd72e84c3c42fa272ed53b1f71`. + +This is a source and release-artifact qualification. It is not authorization +for integration into any external pipeline or production environment. + +## Release Ancestry + +| Boundary | Commit or tag | Role | +| --- | --- | --- | +| Upstream STAR | `2.7.11b` at `b1edc1208d91a53bf40ebae8669f71d50b994851` | Compatibility oracle and inherited core | +| BlackSTAR prior release | `2.7.11b-blackstar.1` at `821457378fa38bfb23b061b8f11ee0a09431dda7` | Qualified index, insertion, integrity, and deployment boundary | +| Cumulative alignment qualification | `9998c445c5b87adacd2a4663bd964ce744aea300` | Qualified H01+A02+A05+A06 source and evidence | +| BlackSTAR current release | `2.7.11b-blackstar.2` | Prior release plus the qualified cumulative alignment stack | + +The complete `blackstar.1` acceptance record is preserved at +[2.7.11b-blackstar.1-acceptance.md](2.7.11b-blackstar.1-acceptance.md). + +## Supported Boundary + +The supported target is x86-64 Linux and includes: + +- deterministic, memory-adaptive `genomeGenerate` suffix-array, SAindex, and + junction-index construction; +- `genomeInsert Full`, packaged `Overlay`, and cached `Delta` modes with + insert-only GTF support; +- virtual-SA no-junction Delta alignment; +- strict package identity, validation, and atomic publication; +- the upstream correctness fixes carried by BlackSTAR; +- recovery from inherited OpenMP affinity narrowing before alignment pthread + creation; +- adaptive record-safe alignment input chunks at 64 or more threads; +- NUMA-aware placement for eligible high-thread private genome loads; and +- transcript-recursion copy elision on nonmutating branches. + +The release excludes `alignReadsMulti`, persistent prefork workers, threaded +BAM-compression prototypes, A01 touched-bin reset, A02b producer/consumer +queue, and A09 LTO/PGO variants. Their source is not stacked into the release. + +## Closed Alignment Findings + +| Finding | Resolution | Acceptance evidence | +| --- | --- | --- | +| Explicit OpenMP binding could confine all STAR alignment pthreads to one physical core | Restore the complete allowed OpenMP-place union before pthread creation; avoid OpenMP initialization when recovery is not needed | Matched failure control, ordinary-path noninferiority, focused sanitizer test, and Q01 repeat | +| Coarse high-thread input chunks produced a long completion tail | Select 1 MB record-safe chunks automatically at 64 or more mapping threads while retaining legacy behavior below the threshold | Replicated uncompressed and compressed gains, lower RSS, exact outputs, and canonical BAM | +| Private genome pages incurred avoidable NUMA migration and fault work | Interleave eligible high-thread private loads while preserving inherited policy, low-thread fallback, and shared-memory behavior | Replicated 96- and 64-thread gates, policy matrix, shared-index fallback, portability test, and Q01 | +| Recursive transcript search copied unchanged state on exclude branches | Pass the current transcript by const reference and copy only mutating or terminal branches | Copy-constructor CPU reduction, five-pair end-to-end gain, compressed input, canonical BAM, and sanitizers | +| Individually accepted changes lacked one cumulative release-style gate | Compare the full stack with the qualified prior release across input, memory, output, affinity, compatibility, and package paths | Q01 cumulative qualification | + +## Cumulative Performance + +The primary public workload used GRCh38 with Ensembl 114 annotations, +12,768,316 paired 76-base ENCODE reads, local SSD, 96 requested logical CPUs, +gene counts, and three seeded order-balanced pairs per input mode. + +| Input | `blackstar.1` control | `blackstar.2` source | Median paired improvement | 95% interval | Correctness | +| --- | ---: | ---: | ---: | ---: | ---: | +| Uncompressed | 84.28 s | 56.17 s | **33.2025%** | 33.0802 to 35.8089% | 3/3 | +| `zcat` | 90.61 s | 64.03 s | **28.5399%** | 28.4661 to 29.6105% | 3/3 | + +Both arms remained below the 3% variability gate. Median peak RSS fell by +6.81% uncompressed and 6.77% through `zcat`. Every pair matched +timing-independent final metrics, splice junctions, and gene counts. + +These values are cumulative measurements against `blackstar.1`; individual +experiment percentages must not be added to them. They apply to the measured +host, corpus, index, storage, and thread count. + +## Affinity Failure Recovery + +A one-pair positive control deliberately set: + +```text +OMP_PROC_BIND=close +OMP_PLACES=cores +``` + +with 96 requested STAR threads and 2,000,000 public read pairs. +`blackstar.1` took 226.13 seconds at 186% mean CPU. The cumulative source +restored 128 OpenMP places and 256 allowed CPUs, then completed in 37.69 +seconds at 952% mean CPU. All three timing-independent comparisons passed. + +The 83.33% reduction is a matched failure-mode result, not an ordinary-path +performance estimate. H01's separate five-pair unbound gate was noninferior. + +## Shared Index and BAM Safety + +The cumulative source preloaded a 29,940,711,542-byte shared genome segment, +retained the default shared-memory NUMA policy, mapped 2,000,000 public paired +reads with `LoadAndKeep`, emitted an unsorted BAM, and passed all five checks: + +- timing-independent final metrics; +- splice junctions; +- gene counts; +- BAM presence; and +- canonical BAM records. + +The control and candidate canonical BAM digest was +`e3f67bccb149277f6f9673e204071b80afab5021c8182028ec17678cdbc750ac`. +`LoadAndRemove` then removed the test segment. This one-pair check establishes +safety, not shared-index performance. + +## Inherited Index and Insertion Qualification + +The `blackstar.1` qualification remains applicable because H01, A02, A05, and +A06 do not change index formats or genome-insert package formats. + +Inherited gates include: + +- three order-balanced full-CHM13 index-build pairs with 49.48% less mean wall + time than upstream STAR and 42/42 substantive file comparisons byte-exact; +- 24/24 Full, Overlay, and Delta hardening checks; +- 4/4 serial, bounded-parallel, and RAM-constrained SAindex strategy checks; +- named-sequence FASTA and GTF insertion, namespace collision rejection, + package relocation, stale-base rejection, corruption rejection, and atomic + cleanup; and +- full-index alignment compatibility under official upstream STAR 2.7.11b. + +## Automated and Reproducibility Gates + +The release-candidate source passed: + +- architecture provenance and public-hygiene validation; +- deterministic rendering of all canonical SVG figures; +- benchmark-harness regression tests; +- eight focused ASan/UBSan scripts; +- the deployment-selector matrix, including expected negative cases; +- 24 genome-insert hardening checks; +- four SAindex strategy checks; and +- clean reproducible release-package builds with OpenMP linkage. + +The final deployable binary and archive are built from the clean protected +`master` commit. Their exact checksums belong in `build-info.tsv`, the checksum +sidecar, and GitHub release metadata. Embedded Git provenance means a +documentation-only promotion commit changes the executable checksum, so the +final artifact checksum is not edited back into this source record. + +## Rejected Toolchain Variants + +LTO and PGO each preserved exact measured outputs and reduced binary size. +Their independent five-pair median improvements were 1.21% and 1.20%, +respectively, below the predeclared 2% practical gate. Neither variant is +included in `blackstar.2`. + +## Residual Risk + +- Performance qualification covers one x86-64 Linux host, one GCC version, + one public paired short-read corpus, one index, one high-thread count, and + local SSD. +- Long reads, single-end performance, network-storage performance, BAM sorting, + macOS, and non-x86-64 targets were not performance-qualified. +- NUMA auto-placement activates only for eligible high-thread private loads; + callers can select `--genomeLoadNumaPolicy Default` to retain inherited + placement. +- Overlay and Delta still require `NoSharedMemory`. +- Bundled HTSlib remains old and is not an accepted base for new compression + work. +- Broad process-lifetime allocations and longstanding inherited compiler + warnings remain outside this release. + +These limitations constrain claims and deployment scope; they are not observed +correctness regressions in the supported release boundary. + +## Publication Requirements + +1. Push the release-candidate branch and require the protected + `build-and-test` check. +2. Preserve linear history when promoting the tested commit to `master`. +3. Create annotated tag `2.7.11b-blackstar.2` at the exact tested `master` + commit. +4. Build and publish the exact clean-commit binary, deterministic archive, + checksum sidecar, `build-info.tsv`, and `ldd.txt`. +5. Verify the live default branch, tag target, required check, release assets, + and asset digests after publication. + +Publication does not authorize deployment into an external pipeline. diff --git a/extras/maintenance/validate_successor_metadata.py b/extras/maintenance/validate_successor_metadata.py index 0151b4e6..a502f3e7 100755 --- a/extras/maintenance/validate_successor_metadata.py +++ b/extras/maintenance/validate_successor_metadata.py @@ -73,6 +73,14 @@ def main() -> int: if compatibility_version != "2.7.11b": raise ValueError("unexpected STAR compatibility base") + release_notes = repo / "docs" / "releases" / ( + f"{blackstar_version}-release-notes.md" + ) + if not release_notes.is_file(): + raise ValueError( + f"release notes missing for BlackSTAR {blackstar_version}" + ) + defaults = (repo / "source" / "parametersDefault").read_text(encoding="utf-8") default_match = re.search(r"^versionGenome\s+(\S+)", defaults, re.MULTILINE) if not default_match or default_match.group(1) != genome_format_version: @@ -95,6 +103,10 @@ def main() -> int: compatibility = (repo / "docs" / "COMPATIBILITY.md").read_text( encoding="utf-8" ) + changelog = (repo / "CHANGELOG.md").read_text(encoding="utf-8") + release_boundary = (repo / "docs" / "BLACKSTAR_RELEASE.md").read_text( + encoding="utf-8" + ) versioning = (repo / "docs" / "VERSIONING.md").read_text(encoding="utf-8") forbidden = [ "github.com/alexdobin/STAR/issues", @@ -114,6 +126,16 @@ def main() -> int: raise ValueError(f"{label} missing from compatibility contract") if executable_version not in versioning: raise ValueError("current executable identity missing from versioning policy") + for text, label in ( + (readme, "README"), + (changelog, "changelog"), + (release_boundary, "release boundary"), + (release_notes.read_text(encoding="utf-8"), "release notes"), + ): + if blackstar_version not in text: + raise ValueError( + f"BlackSTAR release {blackstar_version} missing from {label}" + ) settings = json.loads( (repo / ".github" / "repository-settings.json").read_text( @@ -132,8 +154,17 @@ def main() -> int: }: raise ValueError("unexpected successor repository feature policy") contexts = settings["branch_protection"]["required_status_checks"]["contexts"] - if contexts != ["build-and-test"]: - raise ValueError("required branch-protection check is not build-and-test") + expected_contexts = [ + "build-and-test", + "compiler-gcc", + "compiler-clang", + "starlong-build-and-smoke", + "release-portability", + "codeql-c-cpp", + "codeql-python", + ] + if contexts != expected_contexts: + raise ValueError("required branch-protection checks do not match policy") for relative in required: text = (repo / relative).read_text(encoding="utf-8") diff --git a/extras/scripts/buildBlackSTARRelease.sh b/extras/scripts/buildBlackSTARRelease.sh index e0e2fa66..8c371d09 100755 --- a/extras/scripts/buildBlackSTARRelease.sh +++ b/extras/scripts/buildBlackSTARRelease.sh @@ -57,7 +57,20 @@ package_name="blackstar-${version}-linux-x86_64-${cpu_target}" package_final="${dist_root}/${package_name}" archive="${dist_root}/${package_name}.tar.gz" sbom="${dist_root}/${package_name}.spdx.json" -if [[ -e "${package_final}" || -e "${archive}" || -e "${archive}.sha256" || -e "${sbom}" ]]; then +binary_asset="${dist_root}/${package_name}.STAR" +binary_checksum="${binary_asset}.sha256" +build_info_asset="${dist_root}/${package_name}.build-info.tsv" +compatibility_asset="${dist_root}/${package_name}.compatibility.tsv" +ldd_asset="${dist_root}/${package_name}.ldd.txt" +if [[ -e "${package_final}" || + -e "${archive}" || + -e "${archive}.sha256" || + -e "${sbom}" || + -e "${binary_asset}" || + -e "${binary_checksum}" || + -e "${build_info_asset}" || + -e "${compatibility_asset}" || + -e "${ldd_asset}" ]]; then echo "ERROR: release destination already exists for ${package_name}" >&2 exit 1 fi @@ -168,6 +181,11 @@ mv "${package_dir}" "${package_final}" mv "${archive_staged}" "${archive}" mv "${checksum_staged}" "${archive}.sha256" cp -p "${package_final}/sbom.spdx.json" "${sbom}" +cp -p "${package_final}/STAR" "${binary_asset}" +printf '%s %s\n' "${binary_sha256}" "$(basename "${binary_asset}")" > "${binary_checksum}" +cp -p "${package_final}/build-info.tsv" "${build_info_asset}" +cp -p "${package_final}/compatibility.tsv" "${compatibility_asset}" +cp -p "${package_final}/ldd.txt" "${ldd_asset}" rmdir "${stage_root}" stage_root="" trap - EXIT diff --git a/extras/scripts/compareBlackSTARReleases.sh b/extras/scripts/compareBlackSTARReleases.sh index 78d216eb..b798c203 100755 --- a/extras/scripts/compareBlackSTARReleases.sh +++ b/extras/scripts/compareBlackSTARReleases.sh @@ -26,6 +26,11 @@ products=( "${package}.tar.gz" "${package}.tar.gz.sha256" "${package}.spdx.json" + "${package}.STAR" + "${package}.STAR.sha256" + "${package}.build-info.tsv" + "${package}.compatibility.tsv" + "${package}.ldd.txt" "${package}/STAR" "${package}/build-info.tsv" "${package}/compatibility.tsv" diff --git a/source/VERSION b/source/VERSION index d8eebd05..dbb398df 100644 --- a/source/VERSION +++ b/source/VERSION @@ -1,5 +1,5 @@ #define STAR_VERSION "2.7.11b-blackstar.3" -#define BLACKSTAR_VERSION "1.0.0" +#define BLACKSTAR_VERSION "1.1.0" #define STAR_COMPATIBILITY_VERSION "2.7.11b" #define BLACKSTAR_GENOME_FORMAT_VERSION "2.7.4a" #ifndef BLACKSTAR_CPU_TARGET From 736ff4961bab72c701b7805d095daed4026e094b Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:20:35 +0000 Subject: [PATCH 17/22] Bind release architecture to preparation commit --- docs/architecture/figures.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/architecture/figures.json b/docs/architecture/figures.json index 0c15c25b..2da824f8 100644 --- a/docs/architecture/figures.json +++ b/docs/architecture/figures.json @@ -298,7 +298,7 @@ "title": "Version and compatibility identities", "maturity": "accepted", "visibility": "public", - "code_commit": "b10c14c6515b62f3e730c4e25a3b6dca20caa508", + "code_commit": "cba8e2dbf1813bbe0c5a9889f6d2f2137e632ddb", "source": "docs/architecture/diagrams/src/F25-version-compatibility.mmd", "evidence": ["docs/VERSIONING.md", "docs/COMPATIBILITY.md", "source/VERSION", "source/Parameters.cpp"], "caption": "Independent BlackSTAR release version, executable lineage token, pinned STAR compatibility base, and genome-format version are separate identities exposed together through machine-readable version metadata.", @@ -346,7 +346,7 @@ "title": "BlackSTAR 1.1.0 qualification and release path", "maturity": "accepted", "visibility": "public", - "code_commit": "b10c14c6515b62f3e730c4e25a3b6dca20caa508", + "code_commit": "cba8e2dbf1813bbe0c5a9889f6d2f2137e632ddb", "source": "docs/architecture/diagrams/src/F29-v1.1-release-qualification.mmd", "evidence": ["docs/BLACKSTAR_ACCEPTANCE.md", "docs/RELEASE_POLICY.md", "docs/benchmarks/RCQ-1.1.0-source-20260726/qualification-receipt.tsv"], "caption": "BlackSTAR 1.1.0 promotion requires exact-candidate cumulative qualification, seven protected GitHub checks, a protected-main tag, two byte-identical tagged builds, and attested immutable publication.", From 2d2721879d94031d0db7a62440c17e9bc2fa6f2f Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:26:12 +0000 Subject: [PATCH 18/22] Avoid duplicate pull request CI runs --- .github/workflows/blackstar-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index 75df87e7..f9267468 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -2,6 +2,8 @@ name: BlackSTAR CI on: push: + branches: + - main pull_request: workflow_dispatch: From be2cc51a830d555875c0e841e715bcf2f0a7d240 Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:29:55 +0000 Subject: [PATCH 19/22] Trust container checkout for portable builds --- .github/workflows/blackstar-ci.yml | 1 + .github/workflows/release.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index f9267468..c0465ed7 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -300,6 +300,7 @@ jobs: DIST_DIR: ${{ runner.temp }}/portable-dist JOBS: "2" run: | + git config --global --add safe.directory "${GITHUB_WORKSPACE}" CPU_TARGET=baseline extras/scripts/buildBlackSTARRelease.sh CPU_TARGET=avx2 extras/scripts/buildBlackSTARRelease.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aaaf814c..40b0c7c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,9 @@ jobs: fetch-depth: 0 ref: ${{ inputs.tag }} + - name: Trust container checkout + run: git config --global --add safe.directory "${GITHUB_WORKSPACE}" + - name: Validate release identity run: | [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] From 3b30c04a362592ebde8cfa68ecb0d087379b36ed Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:33:09 +0000 Subject: [PATCH 20/22] Install xxd in portable release builds --- .github/workflows/blackstar-ci.yml | 2 +- .github/workflows/release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index c0465ed7..ff0b6564 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -289,7 +289,7 @@ jobs: DEBIAN_FRONTEND: noninteractive run: | apt-get update - apt-get install -y binutils g++ git gzip make python3 zlib1g-dev + apt-get install -y binutils g++ git gzip make python3 xxd zlib1g-dev - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40b0c7c4..45b608e4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: DEBIAN_FRONTEND: noninteractive run: | apt-get update - apt-get install -y binutils g++ git gzip make python3 zlib1g-dev + apt-get install -y binutils g++ git gzip make python3 xxd zlib1g-dev - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: From 917480fe6fe77d5bbf34c0eb20d1b8077e67975a Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:40:01 +0000 Subject: [PATCH 21/22] Use POSIX portability assertions --- .github/workflows/blackstar-ci.yml | 6 ++++-- .github/workflows/release.yml | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/blackstar-ci.yml b/.github/workflows/blackstar-ci.yml index ff0b6564..14ac6ccc 100644 --- a/.github/workflows/blackstar-ci.yml +++ b/.github/workflows/blackstar-ci.yml @@ -314,9 +314,11 @@ jobs: minimum_glibc="$(awk -F '\t' '$1=="minimum_glibc" {print $2}' "${compatibility}")" dpkg --compare-versions "${minimum_glibc}" le "2.31" done - grep -Fxq $'ymm_instructions\tabsent' \ + awk -F '\t' \ + '$1=="ymm_instructions" && $2=="absent" {found=1} END {exit !found}' \ "${DIST_DIR}/blackstar-${version}-linux-x86_64-baseline/compatibility.tsv" - grep -Fxq $'ymm_instructions\tpresent' \ + awk -F '\t' \ + '$1=="ymm_instructions" && $2=="present" {found=1} END {exit !found}' \ "${DIST_DIR}/blackstar-${version}-linux-x86_64-avx2/compatibility.tsv" - name: Verify release variant result equivalence diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 45b608e4..54f3fcbd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,9 +77,11 @@ jobs: minimum_glibc="$(awk -F '\t' '$1=="minimum_glibc" {print $2}' "${package}/compatibility.tsv")" dpkg --compare-versions "${minimum_glibc}" le "2.31" done - grep -Fxq $'ymm_instructions\tabsent' \ + awk -F '\t' \ + '$1=="ymm_instructions" && $2=="absent" {found=1} END {exit !found}' \ "${RUNNER_TEMP}/build-1/blackstar-${version}-linux-x86_64-baseline/compatibility.tsv" - grep -Fxq $'ymm_instructions\tpresent' \ + awk -F '\t' \ + '$1=="ymm_instructions" && $2=="present" {found=1} END {exit !found}' \ "${RUNNER_TEMP}/build-1/blackstar-${version}-linux-x86_64-avx2/compatibility.tsv" - name: Verify release variant result equivalence From 1fc8860076facd49e0f9093db8da167e8512caee Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 26 Jul 2026 22:59:29 +0000 Subject: [PATCH 22/22] Validate release tags under POSIX shell --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54f3fcbd..28a4bd6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,7 +38,7 @@ jobs: - name: Validate release identity run: | - [[ "${TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + printf '%s\n' "${TAG}" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' version="$(sed -n 's/^#define BLACKSTAR_VERSION "\(.*\)"$/\1/p' source/VERSION)" test "${TAG}" = "v${version}" git cat-file -e "${TAG}^{tag}"