The way I do it is by maintaining a grid of grass chunks around the camera, within a limited area. When a chunk gets too far, I remove it. When the area has empty spots for a chunk, I add them. This can be in a circle or a square. Chunks allow to do a bit of frustrum culling and manage "infinite" presence of grass.
Then each chunk contains a multimesh, and here there are a few options: either generate each instance position using raycasts and procedural formulas with a script, or precalculate one with full density, share it on all multimeshes and rather decimate instances from a vertex shader according to a density map or splatmap (commonly seen with heightmap terrain). If the world is too big to fit in a single heightmap, they could be easily chunked themselves on a larger scale.
Finally, to smooth out the "popping" effect when chunks load/unload, I fade grass using a fragment shader based on distance. The distance is choosen in such a way chunks that get loaded or unload should be almost invisible as all grass inside has faded away.
Properly choosing colors for ground and grass also helps to make it blend in.
Here is how my version looks like with some debug drawing: https://www.youtube.com/watch?v=nrkvvzZ51Js