Show Entries based on content in a Matrix Field
I was finally able to piece this together using Brad's explanation of MatrixBlocks parameters and this thread Can I get an entry by Matrix Block ID?

Seems merge([block.owner.id]) was the piece I was missing to fold the MatrixBlock query back into an entries loop.

{# Find Matrix Blocks with field value #}
{% set blocks = craft.matrixBlocks.type('variant').fieldId(33).salePrice(':notempty:').all() %}

{# Get entry IDs #}
{% set saleEntryIds = [] %}
{% for block in blocks %}
    {% set saleEntryIds = saleEntryIds|merge([block.owner.id]) %}
{% endfor %}

{# Get entries with limit #}
{% set saleProducts = craft.entries.section('shop').id(saleEntryIds).limit(4) %}

{% for product in saleProducts.all() %}
    {{ product.title }}
    {% for block in product.productVariants.all() %}
        {{ block.salePrice }}
    {% endfor %}
{% endfor %}


Seems a little overly complicated - maybe there's an easier way to do it?