mspass-team / mspass-team/mspass_tutorial

ensemble member vector hogging memory

Open
#34 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Jupyter Notebook
Stars
9
Forks
8
PR merge metrics
No merged PRs in 30d

Description

This is a solved problem, but I'm pushing it as an issue to add text to address the fundamental problem it defines. We need to develop guidance to avoid this problem.

The problem is defined by this function used in a workflow I developed for extracting segmented, event waveforms from continuous data. This was my original function that seemed to create a memory problem:

def merge_ensembles(enslist,yearday_key="jday_tag")->tuple:
    """
    Does an operation a bit like Undertaker.bring_out_your dead. 
    That is it returns a tuple with 0 containing a single TimeSeriesEnsemble 
    of all live data and 1 containing a list of documents of read failures. 
    It differs from bring_out_your_dead because input, enslist, is a list of 
    ensembles - some of which are good and some of which are failures.
    """
    ens_merged = TimeSeriesEnsemble()
    failures = list()
    for ens in enslist:
        if ens.live:
            if yearday_key not in ens_merged:
                if yearday_key in ens:
                    ens_merged[yearday_key] = ens[yearday_key]
            for d in ens.member:
                ens_merged.member.append(d)
        else:
            # let this throw an exception if the required metadata are missing 
            # that shouldn't happen since this is internal-beware if this is copied
            doc = dict(ens)
            elogdoc = elog2doc(ens.elog)
            doc['elog_content'] = elogdoc
            failures.append(doc)
    if len(ens_merged)>0:
        ens_merged.set_live()
    return [ens_merged,failures]

It was used in a completion function run with sliding_window_pipeline as follows:

def save_jday_outputs(reader_output,
                         dbname_or_handle,
                         s3fshandle,
                         bucket,
                         year,
                         session=None,
                         yearday_key="jday_tag",
                         s3_fail_collection="s3_read_failures",
                         verbose=False,
                    )->list:
    """
    Saves the segments extracted from day files with the get_s3_segments function
    as pickle files in the S3 bucket pointed to by s3fshandle.


    Any ensembles marked dead will be handled specially with the function 
    bury_s3_read_failures.  
    """
    # put here as this is a common print message for any write errors
    write_message0 = f"save_jday_outputs (WARNING):  failed trying to send serialized data to s3 bucket={bucket}\n"
    s3_client = fetch_s3_client(session)
    db = fetch_dbhandle(dbname_or_handle)
    # first clean the inputs to separate the failures from the live data
    ens,failures = merge_ensembles(reader_output)
    del reader_output
    if len(failures)>0:
        db[s3_fail_collection].insert_many(failures)
        nfailed=len(failures)
        del failures
    if ens.dead():
        # if the ensemble is dead here it is empty so no reason to continue
        return False
    if yearday_key in ens:
        yrdaystr = ens[yearday_key]
        objname_base = yrdaystr
    else:
        objname_base = str(ObjectId())
        print(f"save_segment_reader (WARNING): ensemble Metadata is missing yearday_key={yearday_key}\n")
        print("Using unique id string generated wity pymongo ObjectId=",objname_base)
    s3object_key = f"{bucket}/{year}/{objname_base}.pickle"
    if verbose:
        print("Saving ",len(ens.member)," ensembles to ",s3object_key," Number of failures=",nfailed)
    try:
        with s3fshandle.open(s3object_key,"wb") as fh:
            pickle.dump(ens,fh)
        del ens
        return True
    except Exception as e:
        print("Failed to write ",s3object_key)
        print(e)
        del ens
        return False

A solution suggested by an AI that seems to work is to implement merge_enembles as follows:

def merge_ensembles(enslist, yearday_key="jday_tag"):
    """Merge in order and consume the input list, replacing entries with None.

    Called only for a disposable worker result. A Python parameter deletion
    cannot release the pipeline's reference to that list; clearing its entries
    releases source ensembles incrementally. Members are still copied into the
    output, but the complete input need not coexist with serialization state.
    """
    count = sum(len(ens.member) for ens in enslist if ens.live)
    merged = TimeSeriesEnsemble(count)
    failures = []
    for index in range(len(enslist)):
        ens = enslist[index]
        try:
            if ens.live:
                if yearday_key not in merged and yearday_key in ens:
                    merged[yearday_key] = ens[yearday_key]
                for member in ens.member:
                    merged.member.append(member)
                # A pybind member wrapper can keep its source vector alive.
                if len(ens.member):
                    del member
            else:
                document = dict(ens)
                document["elog_content"] = elog2doc(ens.elog)
                failures.append(document)
        finally:
            enslist[index] = None
            del ens
    if len(merged.member):
        merged.set_live()
    return [merged, failures]

It left save_jday_outputs largely the same but it removed the explicit del ens commands in that function.

Long background but the question is why the AI solution works but the original causes a memory bloat? The AIs typically obscure comments suggest some fundamental problem with you pybind11 handles binding std::vector containers to python. Namely "A pybind11 member wrapper can keep its source vector alive." Anyone have clue what that means? Makes no sense to me that deleting the members is necessary. What I did originally seems cleaner which was delete the entire list (enslist in the function arg) after the code was done with it. Why does the AI code not cause bloat but the other did?

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Review the merge_ensembles and save_jday_outputs examples in the issue, including how the disposable worker result is handled. The documentation is done when it gives clear guidance on the reported memory behavior and explains why deleting list entries differs from deleting the local list reference, without requiring readers to infer the rationale from the example.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
documentation
Issue type
Documentation
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.