Skip to content

concurrent popper on the same thread, inspired by rpgoldman - #148

Open
DARKMOONlite wants to merge 3 commits into
logic-and-learning-lab:mainfrom
DARKMOONlite:main
Open

concurrent popper on the same thread, inspired by rpgoldman#148
DARKMOONlite wants to merge 3 commits into
logic-and-learning-lab:mainfrom
DARKMOONlite:main

Conversation

@DARKMOONlite

@DARKMOONlite DARKMOONlite commented Jul 28, 2026

Copy link
Copy Markdown

Issue:

running popper in the same process doesn't work due to conflict between the popper instances. to run even using a ProcessPoolThread requires the max_tasks_per_child parameter set to 1 which caused some issues with running Popper for me. since then I've been using @rpgoldman's branch to get around this issue, but this branch is out of date, and is missing out on a lot of modern improvements to popper since the big refactoring.
e.g.

        with ProcessPoolExecutor(max_workers=args.threads,max_tasks_per_child=1) as executor:
            directory_results = executor.map(directory_runner, directories)
            for directory, csv_row in tqdm(directory_results, total=len(directories), desc="Running Popper tasks"):
                csv_path = directory / f"{'maxsynth' if args.maxsynth else 'popper'}_results.csv" if not args.file_name else directory / Path(args.file_name)
                write_results_to_csv(csv_row, csv_path)

Fix

  • every time the Tester class is created it creates a unique uuid to create a unique and identifiable popper module.
        self.module_name = 'popper_tester_module_' + str(uuid.uuid4().int)
  • all calls to janus functions are then wrapped in the query_once function which loads all relevant facts into that specific module name (the original method is loaded as janus_query_once) this is done to reduce coding footprint, other naming conventions are usable idk.
def query_once(query,inputs={},keep=False,truth_vals=TruthVal.PLAIN_TRUTHVALS,module_name=None):
    """A wrapper function for janus_swi.query_once that formats the query string and passes the module name.
    """
    if module_name is None: # if no module name is provided, just call the original query_once function
        return janus_query_once(query, inputs=inputs, keep=keep, truth_vals=truth_vals)
    return janus_query_once(f"{module_name}:({query.strip().rstrip('.')})", inputs=inputs, keep=keep, truth_vals=truth_vals)
  • the test.pl file that is shared between experiments cannot be loaded into the module because it must be shared to all ongoing experiments. to get around this I create a temporary file, fill it with the same contents then pass the temp file to janus.
        # create temporary test_pl_file 
        with tempfile.NamedTemporaryFile(delete=False, suffix=".pl") as tmp:
            tmp.close()
            shutil.copyfile(test_pl_path, tmp.name)
            
            for x in [exs_pl_path, bk_pl_path, tmp.name]:
                if os.name == 'nt': # if on Windows, SWI requires escaped directory separators
                    x = x.replace('\\', '\\\\')
                logger.info(f'Consulting {x}')
                consult(x,module=self.module_name) # this shouldn't be loaded into a module as otherwise other modules cant read these shared files

Result:

the system can now run multiple threads using a ThreadPoolExecutor without background bleed.
@andrewcropper what do you think?

DARKMOONlite and others added 3 commits July 28, 2026 13:14
…er/tester.py by loading the files into modules to keep them more seperate than before, allowing a system to run them concurrently in a single python instance this allows the system to be run with a ThreadPoolExecutor or a ProcessPoolExecutor without needing to specify max_tasks_per_child=1. The only diversion from @rpgoldman's code was using a uuid rather than the current time to create unique names for the tempory prolog module, and the test.pl file is converted to a temporary file before being loaded into the prolog module to get around an issue with prolog where a single file cannot be loaded by multiple modules, im not sure how @rpgoldman's code got around this limiation.
…am unsure if the temporary module is still being deleted but it will be cleaned up when the prolog instance is deleted tho so as long as someone doesn't do hundreds of incredibly large experiments they'll be fine.
…s into the tester class which means we do not need to pass the module around
@andrewcropper

Copy link
Copy Markdown
Collaborator

I revised the code a little to make it a bit simpler. I did not know how to edit this PR (or even if it is possible) so I made a new branch:

https://github.com/logic-and-learning-lab/Popper/tree/fix/tester-module-consult

I think it works as you expect. Can you check?

@DARKMOONlite

DARKMOONlite commented Aug 18, 2026

Copy link
Copy Markdown
Author

sorry, had some pressing home matters to address.
The branch looks good. I've tested them on my system and they seem to work fine.
It should be noted that there is a small performance difference between multiple instances of popper across multiple threads vs multiple processes, so the default should still be a process pool, but now this should allow some more flexibility in how its used.
merged your commit into the pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants