Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make nonnative gadget params configurable #129

Open
wants to merge 4 commits into
base: master
Choose a base branch
from

Conversation

slumber
Copy link
Contributor

@slumber slumber commented Sep 20, 2023

Depending on a goal, some developers would want to have a specific configuration perhaps with a smaller number
of limbs.

This change is almost backwards compatible and actually breaks some code in ark-crypto-primitives,
but it tries to follow the pattern from Rust std like "Vec<T, A: Allocator = Global>"

Hence the new definition

pub enum NonNativeFieldVar<
    TargetField: PrimeField,
    BaseField: PrimeField,
    P: Params = DefaultParams, // <-- opt in to define your own.
> {
    /// Constant
    Constant(TargetField),
    /// Allocated gadget
    Var(AllocatedNonNativeFieldVar<TargetField, BaseField, P>),
}

Likewise for AllocatedNonNativeFieldVar, NonNativeFieldMulResultVar, AllocatedNonNativeFieldMulResultVar.


Before we can merge this PR, please make sure that all the following items have been
checked off. If any of the checklist items are not applicable, please leave them but
write a little note why.

  • Targeted PR against correct branch (master)
  • Linked to Github issue with discussion and accepted design OR have an explanation in the PR that describes this work.
  • Wrote unit tests
  • Updated relevant documentation in the code
  • Added a relevant changelog entry to the Pending section in CHANGELOG.md
  • Re-reviewed Files changed in the Github PR explorer

@slumber
Copy link
Contributor Author

slumber commented Sep 28, 2023

@Pratyush @weikengchen can you please take a look?

@mmagician
Copy link
Member

@slumber Could you also provide the corresponding fix for crypto-primitives?

use super::NonNativeFieldConfig;

/// Type that provides non native arithmetic configuration for a given pair of fields
/// and optimization goal.
pub trait Params: fmt::Debug + 'static {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit too close to Config; maybe we should rename it to something like ConfigBuilder?

Also, does it make sense to make TargetField and BaseField associated types on this trait? This would reduce the number of type parameters everywhere else.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weikengchen what do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit too close to Config; maybe we should rename it to something like ConfigBuilder?

*Builder is usually used for builder pattern, like

// build runtime
let runtime = Builder::new_multi_thread()
    .worker_threads(4)
    .thread_name("my-custom-name")
    .thread_stack_size(3 * 1024 * 1024)
    .build()
    .unwrap();

Params name is indeed very generic, it could be FindConfig or ComputeParams or whatever. I chose Params because you can always do

use ark_r1cs_std::fields::nonnative::params::Params as FindNonNativeConfig;

But I'm OK with changing it to any other name.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, does it make sense to make TargetField and BaseField associated types on this trait? This would reduce the number of type parameters everywhere else.

This would cause DefaultParams to become generic, as you'll need DefaultParamsF1F2 for every field pair, because the trait can only be implemented once.

It's a question of whether Params trait is defined for any pair of fields or for one particular. Given the original design, I believe it's the former, and this is reflected in the code.

@slumber
Copy link
Contributor Author

slumber commented Sep 29, 2023

@slumber Could you also provide the corresponding fix for crypto-primitives?

I will once we're done with this PR.

@weikengchen weikengchen requested a review from a team as a code owner September 30, 2023 06:58
@weikengchen weikengchen requested review from z-tech, mmagician and weikengchen and removed request for a team September 30, 2023 06:58
@slumber
Copy link
Contributor Author

slumber commented Oct 10, 2023

@weikengchen @Pratyush can we have another iteration on this one?

@Pratyush
Copy link
Member

Pratyush commented Jan 3, 2024

@slumber, sorry for the delay on this! Could you provide some example alternative instantiations of Params? That way we can get a sense of the why the new API looks the way it does. Thanks!

@slumber
Copy link
Contributor Author

slumber commented Jan 8, 2024

@slumber, sorry for the delay on this! Could you provide some example alternative instantiations of Params? That way we can get a sense of the why the new API looks the way it does. Thanks!

@Pratyush thank you for getting back to the PR.

An example is computing poseidon hash.
I'd like to minimize the number of limbs in emulated variables, but keep "constraints" optimizations target.

This is a sketch, but the idea should be clear;
Knowing that the minimum legal number of limbs is 3 (#128):

const MIN_SUPPORTED_LIMBS: usize = 3;

pub struct MinLimbsParams;

impl Params for MinLimbsParams {
    fn get<TargetField: PrimeField, BaseField: PrimeField>(
        _optimization_type: OptimizationType, // always minimize
    ) -> NonNativeFieldConfig {
        let target_bits = TargetField::MODULUS_BIT_SIZE;
        let base_bits = BaseField::MODULUS_BIT_SIZE;

        let num_limbs = if target_bits >= base_bits {
            MIN_SUPPORTED_LIMBS
        } else {
            (base_bits / target_bits).max(MIN_SUPPORTED_LIMBS)
        };
        let bits_per_limb = (base_bits / num_limbs).next_power_of_two();

        NonNativeFieldConfig {
            num_limbs: num_limbs as usize,
            bits_per_limb: bits_per_limb as usize,
        }
        
    }
}

Why put it as a generic parameter:

  1. It's the most flexible option: you can hardcode, use default, use anything custom like I did above.
  2. Keeps the type safety. You can't multiply Var<F1, F2, P1> by Var<F1, F2, P2>.

@Pratyush
Copy link
Member

The primary concern I have is that users will need to specify an additional type parameter whenever they want to use EmulatedFpVar generically.

This happens, for example, here:

+ Mul<EmulatedFpVar<C::ScalarField, ConstraintF>, Output = Self>
.
In this case, we want to support scalar multiplication by any EmulatedFpVar as long as it is for the right field.

Would it be possible to have a different way to track the parameters? E.g. via a Box<dyn EmulationConfig> that's stored within the struct?

@slumber
Copy link
Contributor Author

slumber commented Feb 1, 2024

The primary concern I have is that users will need to specify an additional type parameter whenever they want to use EmulatedFpVar generically.

This happens, for example, here:

+ Mul<EmulatedFpVar<C::ScalarField, ConstraintF>, Output = Self>

.

Yes, this bound wasn't present at the time of writing this PR. Being required to specify config in a trait definition doesn't appeal to me either.

In this case, we want to support scalar multiplication by any EmulatedFpVar as long as it is for the right field.
Would it be possible to have a different way to track the parameters? E.g. via a Box<dyn EmulationConfig> that's stored within the struct?

partially:

If you think it's OK then I'll reimplement it with suggested change.

@slumber
Copy link
Contributor Author

slumber commented Feb 2, 2024

@Pratyush on second thought, I don't think it's going to work because of object safety rules, at least I couldn't make it work in the playground

@slumber
Copy link
Contributor Author

slumber commented Feb 16, 2024

@Pratyush what do you think about storing configuration directly in the struct:

num_limbs: usize,
bits_per_limb: usize,

And whenever you want to use non-default one you provide generic EmulatedConfig, for example to squeeze_non_native or new_variable_with_config

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.

4 participants