In my case the terrain can be modified, it's not a static procedural defined shape.
Hmm... wait, why do people rely on sampling so much? Be a little more procedural with the way you handle things.
I don't see why I couldn't have grass in caves, the greyscale map just has to be three dimensional as you said. But once again, since the terrain can be modified I don't see why I'd want to store the grass density in any map, it should be part of the voxel data.
Grass: Smartest I've seen is a grass map, greyscale map of grass density and then just spawn based on the map. A single easily generatable texture shouldn't be much memory. Of course you're only getting a single height layer, no grass in caves. But then why would you want that? I suppose if you did could always use a multi channel texture to store it and then have up to X heights depending on the channels you have available.
I did plan in fact some sort of merging to describe voxels with grass on top and rock on the sides, but I don't think that having materials that describe each side (or even two) is useful; if there is rock on the sides, so there is on top and everywhere else. I just thought of using a single bit to know whether there should be grass on top or not. For dirt on top and rock on side, just have a voxel layer of dirt on top of rock should be enough.
For your case, I recommend you just describe enough for the sampling to know what's appropriate i.e. a single scalar code for each voxel which corresponds to a 6-sided set of materials Examples: some material sets may entirely include the cliff texture, sometimes a mix of messy grass at the top and cliff on the sides, dirt ontop and red rock on the sides, or chalky-dirt with weeds on the top and cobble on the sides etc.