-
Notifications
You must be signed in to change notification settings - Fork 81
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
Backend unification #417
Backend unification #417
Conversation
e355bd1
to
165dd57
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall I like the direction of this refactoring, simplifying the backend's API to a single entry point. I also like some of the changes to the halo2 crate with the Halo2 struct.
However, before this PR, the backend API made distinctions between provers that need setup and provers that don't in the traits, disallowing bad paths by construction. This PR makes all that runtime dependent in a way that I find kinda hacky and personally don't like. Maybe I'm overreacting and it's fine, but we should discuss it.
@Schaeff should also take a look
backend/src/pilcom_cli/mod.rs
Outdated
} | ||
} | ||
|
||
fn write_constants_to_fs<T: FieldElement>( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the FS stuff should all go to CLI instead of backend
halo2/src/prover.rs
Outdated
|
||
/// Create a halo2 proof for a given PIL, fixed column values and witness column values | ||
/// We use KZG ([GWC variant](https://eprint.iacr.org/2019/953)) and Keccak256 | ||
pub struct Halo2<F: FieldElement> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Halo2Prover maybe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
halo2/src/prover.rs
Outdated
let params = ParamsKZG::<Bn256>::read(&mut params).unwrap(); | ||
prove_ast(pil, fixed, witness, params) | ||
} | ||
pub fn read(input: &mut impl io::Read) -> Result<Self, io::Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
new_with_params maybe
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still think this should be applied
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forgot this one
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like that it has a simple API so I'm not against having it now.
But I can't help but think that this would be better structured with a bunch of traits like these.
As I understand it, this PR uses dynamic dispatch to go from a user-defined backend argument to specific implementations of a single trait. Dynamic dispatch has to happen somehow anyway and I like the way it's done here. But what is stopping us from having many traits instead of one here? So for example when running a setup:
pub trait SetupBackendBuilder<T: FieldElement> {
fn setup_from_params(&self, size: DegreeType) -> Result<Box<dyn SetupBackend<T>>, NotApplicable>;
}
Then backends which don't have that functionality can return an error here, and we can simplify the actual implementations as the underlying traits are simpler, for example AggregationBackend
can have an API geared towards aggregation, etc.
@@ -36,7 +36,7 @@ pub(crate) fn analyzed_to_circuit<T: FieldElement>( | |||
|
|||
let query = |column, rotation| Expr::Var(PlonkVar::Query(ColumnQuery { column, rotation })); | |||
|
|||
let mut cd = CircuitData::from(fixed, witness, &analyzed.constants); | |||
let mut cd = CircuitData::from(fixed.to_owned(), witness, &analyzed.constants); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change seems unrelated to this PR, but doesn't this do a lot of copying now? We could go all the way and make CircuitData
take fixed
as reference, the extra selectors which prevent that can be stored separately.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried, but CircuitData
needs a Vec
as it has to append to it.
If a Vec
is needed inside a function, I have a preference on taking it directly as the argument.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's my point with the selectors: CircuitData
needs to introduce 2 selectors __enable_cur
and __enable_next
. Instead of adding them to the fixed columns, we can just store them in another struct field in CircuitData
, then fixed
doesn't need to be mutable. Then when hitting halo2 we can add these selectors: fixed: cd.fixed.iter().chain(once(cd.sel0)).chain(cd.sel1)
. It's sad to clone the whole fixed data for 2 selectors.
So, @Schaeff and @leonardoalt prefer I separate the backends in trait categories. There are two dimensions:
The cross product gives 4 traits. Is this a good split? Currently we have 3 backends. |
I wouldn't called it Serializable/non-serializable because it's not clear what's being serialized, and also it's not directly related to serialization but rather pre-setup. I would call it something like "ProverWithSetup"/"ProverWithoutSetup" like we had before. The aggregation one, I'm fine with having an |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
comments for the new backend stuff
backend/src/lib.rs
Outdated
witness: Option<&[(&str, Vec<F>)]>, | ||
prev_proof: Option<Proof>, | ||
output_dir: &Path, | ||
) -> io::Result<()>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why doesn't it return a proof?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See this: #417 (comment)
backend/src/lib.rs
Outdated
witness: Option<&[(&str, Vec<F>)]>, | ||
prev_proof: Option<Proof>, | ||
output_dir: &Path, | ||
) -> io::Result<()>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should return proof?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See this: #417 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually thinking about it now I disagree with it. The client should receive the proof and decide what to do with it. The way we're doing this now, no one will be able to use the backend crate via powdr for any needs if the proofs always get written to disk, especially in cases of recursion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it should return option proof
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
impl BackendType { | ||
pub fn build<T: FieldElement>(&self) -> &'static dyn BackendFactory<T> { | ||
#[cfg(feature = "halo2")] | ||
const HALO2_FACTORY: WithSetupFactory<halo2::Halo2Prover> = WithSetupFactory(PhantomData); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does this one use halo2::...
and Mock uses halo2_structs
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because Halo2Prover
is defined in the halo2
crate, and the mock is defined in the module halo2_structs
of the backend
crate that makes use of the halo2
crate.
Had a call with @lvella, missing items until merge:
|
backend/src/halo2_impl.rs
Outdated
}; | ||
|
||
{ | ||
if let Some(output_dir) = output_dir { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now that the proof is returned, we don't need to write it to disk. In general the only crate which has fs access should be powdr_cli
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This in turn should simplify the function signatures as output_dir
shouldn't be required?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Me and Leo talked earlier and we considered all that. I think he prefers your way, but hear my reasons:
- In our current model we have an output dir and dump all the compilation artifacts there, with predefined names. The way I propose, we keep this model, and let the backend control the naming of the artifacts it can generate.
- If we are to keep the pilcom as a backend, it needs to know the output dir, because all it does is write files.
- In every place we call the backend to write files (there are currently two), we want the writing of files to be consistent. So, the natural place for this function to be is in the backend.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the main culprit here is the pilcom backend. It doesn't return a proof and creates a non-proof file that needs to be written to disk in our current workflow.
In general the only crate which has fs access should be powdr_cli.
I agree with this, but I don't know how to solve it for the pilcom artifact because
... So, the natural place for this function to be is in the backend.
I also agree with this.
I would still prefer the proof not to be written here, but that doesn't solve pilcom.
halo2/src/prover.rs
Outdated
params.downsize(degree); | ||
params | ||
}; | ||
pub fn write(&self, output: &mut impl io::Write) -> Result<(), io::Error> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this supposed to be write_setup
or just write
? Either fine with me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, it was supposed to be write_setup
Originally,
Backend
was an enum, whose the crate's user had to match, and then use the specific API for each of the backend, with specific concrete types and functions.This my attempt to make the
Backend
API generic, so that the user doesn't need to know the specifics of the selected backend. This led to the runtime checks being moved from the CLI to theBackend
, and in case the user tries to use a functionality not available in some backend, the error is raised from inside the backend implementation.