Multithreaded loading

I am exploring a workaround for Incorrect BBoxes in BasicWriter output - #43 by Turowicz

Basically I need to load 1000s of instances of the same USD at start, in order to offset the bug with instantization.

Thing is, rep.create.from_usd(usd="file://...",count=NUM_TOTAL_ASSETS) is very slow as it seems to use only a single CPU core.

@pcallender @jlafleche would it be possible to run this in parallel to speed up app start?

Another aspect of that workaround: when having 1000s of instances of a single usd, any kind of randomization operations (chaning materials, colours, etc.) take a lot of time at each interval as there is a massive graph to run the regexes against.

Hello @Turowicz! Accelerating the creation of USD references is something we’ve been actively working on and I’m happy to share a function that you can use now until the next version of Replicator which will integrate this optimization natively. This approach achieve a ~190x speedup

from typing import Optional
import omni.usd
import pxr
import omni.replicator.core as rep


def from_usd_fast(
    usd_path: str = None,
    prim_path: str = None,
    parent: Optional[str] = None,
    count: int = 1,
    name: str = "Ref",
) -> rep.utils.ReplicatorItem:
    """Use Prim Specs to create references within an Sdf.ChangeBlock.

    This function is faster than the default create function because it uses Prim Specs to create references within an
    Sdf.ChangeBlock.

    Args:
        usd_path (str): _description_
        prim_path (str): _description_
        parent (Optional[str], optional): _description_. Defaults to None.
        count (int, optional): _description_. Defaults to 1.
        name (str, optional): _description_. Defaults to "Ref".

    Returns:
        rep.utils.ReplicatorItem: _description_
    """
    usd_path = str(usd_path) if usd_path is not None else ""
    prim_path = str(prim_path) if prim_path is not None else pxr.Sdf.Path()

    stage = omni.usd.get_context().get_stage()
    if parent is None:
        if not stage.GetPrimAtPath(rep.create.REPLICATOR_SCOPE):
            stage.DefinePrim(rep.create.REPLICATOR_SCOPE, "Scope")
        parent = rep.create.REPLICATOR_SCOPE
    xform_paths = []
    prims_paths = []
    suffix = 0
    xform_name = f"{name}_Xform"
    with pxr.Sdf.ChangeBlock():
        for _ in range(count):
            # First create a xform on top of it.
            if suffix == 0:
                xform_path = f"{parent}/{xform_name}"
                suffix += 1
            else:
                xform_path = f"{parent}/{xform_name}_{suffix:02d}"
                suffix += 1

            # Find first available path
            while stage.GetPrimAtPath(xform_path):
                suffix += 1
                xform_path = f"{parent}/{xform_name}_{suffix:02d}"

            spec = pxr.Sdf.CreatePrimInLayer(stage.GetEditTarget().GetLayer(), xform_path)
            spec.typeName = "Xform"
            spec.specifier = pxr.Sdf.SpecifierDef

            # Create the prim
            name = pxr.Tf.MakeValidIdentifier(name)
            ref_prim_path = f"{xform_path}/{name}"
            prim_spec = pxr.Sdf.CreatePrimInLayer(stage.GetEditTarget().GetLayer(), ref_prim_path)
            prim_spec.specifier = pxr.Sdf.SpecifierDef
            xform_paths.append(xform_path)

            prim_spec.referenceList.Prepend(pxr.Sdf.Reference(assetPath=usd_path, primPath=prim_path))
            prims_paths.append(ref_prim_path)

    return rep.create.group(xform_paths)

rep.create.register(from_usd_fast)

Test Script:

import time
N = 5000
ref = rep.example.ASSETS[0]

omni.usd.get_context().new_stage()
print("Starting Standard Test...")
start = time.time()
rep.create.from_usd(ref, count=N)
print("Standard", time.time() - start)

omni.usd.get_context().new_stage()
print("Starting Fast Test...")
start = time.time()
rep.create.from_usd_fast(usd_path=ref, count=N)
print("Fast", time.time() - start)

omni.usd.get_context().new_stage()
print("Starting Standard Test with positions...")
start = time.time()
refs = rep.create.from_usd(ref, count=N)
with refs:
    rep.modify.pose(position=rep.distribution.uniform((-500, -500, -500), (500, 500, 500)))
print("Standard with positions", time.time() - start)

omni.usd.get_context().new_stage()
print("Starting Fast Test with positions...")
start = time.time()
refs = rep.create.from_usd_fast(usd_path=ref, count=N)
with refs:
    rep.modify.pose(position=rep.distribution.uniform((-500, -500, -500), (500, 500, 500)))
print("Fast with positions", time.time() - start)

Output on test (N=5000, machine (Ryzen 1700X, Nvidia TitanRTX))

Starting Standard Test...
Standard 194.00616669654846

Starting Fast Test...
Fast 0.994647741317749

Starting Standard Test with positions...
Standard with positions 194.6883668899536

Starting Fast Test with positions...
Fast with positions 1.2436614036560059
1 Like

We shall see tomorrow 🤣

@jlafleche any ideas how to speed up the code for manipulating tints (below) for 1000s of instances? Seems like the regex search is slow.

        # Randomize accessories tints
        with rep.get.shader(path_pattern="hat_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=neutral()))

        with rep.get.shader(path_pattern="hat_.*_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=neutral()))

        with rep.get.shader(path_pattern="helmet_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=flashy_neutral()))

        with rep.get.shader(path_pattern="helmet_.*_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=flashy_neutral()))

        with rep.get.shader(path_pattern="earprotectors",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=flashy_neutral()))

        with rep.get.shader(path_pattern="mask_A_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=flashy_neutral()))

        with rep.get.shader(path_pattern="hygenic_mask_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=flashy_neutral()))

        with rep.get.shader(path_pattern="gloves_tint",cache_result=False):
            rep.modify.attribute(name="inputs:diffuse_tint",
                                 attribute_type="color3f",
                                 value=rep.distribution.choice(choices=flashy()))

        with rep.get.shader(path_pattern="glasses_12_lenses",cache_result=False):
            rep.modify.attribute(name="inputs:opacity_constant",
                                 attribute_type="float",
                                 value=rep.distribution.uniform(lower=0.05,
                                                                upper=0.13))